From d9cd14d36a6c96e7b283688099f6d9608a7926c6 Mon Sep 17 00:00:00 2001 From: Mattia Lavacca Date: Wed, 27 Jul 2022 18:23:05 +0200 Subject: [PATCH] Hostname Matching helper created Signed-off-by: Mattia Lavacca --- internal/controllers/gateway/route_utils.go | 22 +++++- internal/util/hostname.go | 63 +++++++++++++++ internal/util/hostname_test.go | 83 ++++++++++++++++++++ test/conformance/gateway_conformance_test.go | 4 +- 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 internal/util/hostname.go create mode 100644 internal/util/hostname_test.go diff --git a/internal/controllers/gateway/route_utils.go b/internal/controllers/gateway/route_utils.go index 9163e75de4..d0be165f8e 100644 --- a/internal/controllers/gateway/route_utils.go +++ b/internal/controllers/gateway/route_utils.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -93,6 +94,7 @@ func getSupportedGatewayForRoute(ctx context.Context, mgrc client.Client, obj cl allowedNamespaces := make(map[string]interface{}) // set true if we find any AllowedRoutes. there may be none, in which case any namespace is permitted filtered := false + matchingHostname := false for _, listener := range gateway.Spec.Listeners { // TODO https://github.com/Kong/kubernetes-ingress-controller/issues/2408 // This currently only performs a baseline filter to ensure that routes cannot match based on namespace @@ -101,6 +103,8 @@ func getSupportedGatewayForRoute(ctx context.Context, mgrc client.Client, obj cl // implementation with default allowed kinds when there's no user-specified filter. switch obj.(type) { case *gatewayv1alpha2.HTTPRoute: + hostnames := obj.(*gatewayv1alpha2.HTTPRoute).Spec.Hostnames + matchingHostname = matchHostname(listener, hostnames) if !(listener.Protocol == gatewayv1alpha2.HTTPProtocolType || listener.Protocol == gatewayv1alpha2.HTTPSProtocolType) { continue } @@ -113,6 +117,8 @@ func getSupportedGatewayForRoute(ctx context.Context, mgrc client.Client, obj cl continue } case *gatewayv1alpha2.TLSRoute: + hostnames := obj.(*gatewayv1alpha2.TLSRoute).Spec.Hostnames + matchingHostname = matchHostname(listener, hostnames) if listener.Protocol != gatewayv1alpha2.TLSProtocolType { continue } @@ -147,7 +153,7 @@ func getSupportedGatewayForRoute(ctx context.Context, mgrc client.Client, obj cl } _, allowedNamespace := allowedNamespaces[obj.GetNamespace()] - if !filtered || allowedNamespace { + if (!filtered || allowedNamespace) && matchingHostname { gateways = append(gateways, &gateway) } } @@ -161,3 +167,17 @@ func getSupportedGatewayForRoute(ctx context.Context, mgrc client.Client, obj cl return gateways, nil } + +func matchHostname(listener gatewayv1alpha2.Listener, hostnames []gatewayv1alpha2.Hostname) bool { + if listener.Hostname == nil || *listener.Hostname == "" || len(hostnames) == 0 { + return true + } + + for _, hostname := range hostnames { + if util.Match(string(*listener.Hostname), string(hostname)) { + return true + } + } + + return false +} diff --git a/internal/util/hostname.go b/internal/util/hostname.go new file mode 100644 index 0000000000..d13406159b --- /dev/null +++ b/internal/util/hostname.go @@ -0,0 +1,63 @@ +package util + +import "strings" + +// TODO: comment exported function and code sections +func Match(pattern, value string) bool { + patternParts := strings.Split(pattern, ".") + valueParts := strings.Split(value, ".") + + var i, j int + var wildcardj, wildcardi bool + for i, j = 0, 0; i < len(patternParts) && j < len(valueParts); i, j = i+1, j+1 { + var matchFound bool + + if wildcardj { + for ; i < len(patternParts); i += 1 { + if patternParts[i] == valueParts[j] { + matchFound = true + break + } + } + if !matchFound { + return false + } + } + + if wildcardi { + for ; j < len(valueParts); j += 1 { + if patternParts[i] == valueParts[j] { + matchFound = true + break + } + } + if !matchFound { + return false + } + } + + if (wildcardj || wildcardi) && (len(valueParts)-j != len(patternParts)-i) { + return false + } + + if patternParts[i] == "*" { + wildcardi = true + continue + } + if valueParts[j] == "*" { + wildcardj = true + continue + } + + wildcardi, wildcardj = false, false + + if patternParts[i] != valueParts[j] { + return false + } + } + if len(valueParts)-j != len(patternParts)-i { + return false + } + + return true +} diff --git a/internal/util/hostname_test.go b/internal/util/hostname_test.go new file mode 100644 index 0000000000..66b3ea8d07 --- /dev/null +++ b/internal/util/hostname_test.go @@ -0,0 +1,83 @@ +package util_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" +) + +func TestMatch(t *testing.T) { + for _, tt := range []struct { + name string + pattern string + value string + expected bool + }{ + { + name: "same hostname", + pattern: "test.com", + value: "test.com", + expected: true, + }, + { + name: "different hostname suffix", + pattern: "test.com", + value: "test.net", + expected: false, + }, + { + name: "different hostname prefix", + pattern: "foo.com", + value: "bar.com", + expected: false, + }, + { + name: "different hostname lengths with only suffix matching", + pattern: "foo.test.com", + value: "test.com", + expected: false, + }, + { + name: "different hostname lengths with only prefix matching", + pattern: "test.test.com", + value: "test.test", + expected: false, + }, + { + name: "valid wildcard for one element", + pattern: "*.test.com", + value: "foo.test.com", + expected: true, + }, + { + name: "valid wildcard for many elements", + pattern: "*.example.com", + value: "so.many.names.example.com", + expected: true, + }, + { + name: "not matching wildcard", + pattern: "*.example.com", + value: "example.com", + expected: false, + }, + { + name: "double matching wildcard", + pattern: "*.example.com", + value: "*.example.com", + expected: true, + }, + { + name: "double not matching wildcard", + pattern: "*.example.com", + value: "*.example.net", + expected: false, + }, + } { + // Test that the functions behave in the same way even swapping the parameters + assert.Equal(t, tt.expected, util.Match(tt.pattern, tt.value), tt.name) + assert.Equal(t, tt.expected, util.Match(tt.value, tt.pattern), tt.name) + } +} diff --git a/test/conformance/gateway_conformance_test.go b/test/conformance/gateway_conformance_test.go index deac488c6d..1261878db6 100644 --- a/test/conformance/gateway_conformance_test.go +++ b/test/conformance/gateway_conformance_test.go @@ -79,7 +79,7 @@ var enabledGatewayConformanceTests = sets.NewString( // "HTTPRouteDisallowedKind", //OK // "HTTPExactPathMatching", //OK // "HTTPRouteHeaderMatching", //OK - // "HTTPRouteHostnameIntersection", //FAIL + "HTTPRouteHostnameIntersection", //FAIL // "HTTPRouteInvalidNonExistentBackendRef", //FAIL // "HTTPRouteInvalidBackendRefUnknownKind", //FAIL // "HTTPRouteInvalidCrossNamespaceBackendRef", //FAIL @@ -90,6 +90,6 @@ var enabledGatewayConformanceTests = sets.NewString( // "HTTPRouteMatching", //OK // "HTTPRouteQueryParamMatching", //? // "HTTPRouteReferenceGrant", //? - "HTTPRouteRequestHeaderModifier", //OK + // "HTTPRouteRequestHeaderModifier", //OK // "HTTPRouteSimpleSameNamespace", //OK )