mirror of
https://github.com/traefik/traefik.git
synced 2026-07-08 09:13:52 -04:00
Add Gateway API HTTPRoute CORS filter
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Build and Publish Documentation / Doc Process (push) Has been cancelled
Build experimental image on branch / build-webui (push) Has been cancelled
Build experimental image on branch / Build experimental image on branch (push) Has been cancelled
Test Integration / build (push) Has been cancelled
Test Unit / Unit tests (push) Has been cancelled
Test Unit / test-ui-unit (push) Has been cancelled
Test Integration / test-integration (0, 12) (push) Has been cancelled
Test Integration / test-integration (1, 12) (push) Has been cancelled
Test Integration / test-integration (10, 12) (push) Has been cancelled
Test Integration / test-integration (11, 12) (push) Has been cancelled
Test Integration / test-integration (2, 12) (push) Has been cancelled
Test Integration / test-integration (3, 12) (push) Has been cancelled
Test Integration / test-integration (4, 12) (push) Has been cancelled
Test Integration / test-integration (5, 12) (push) Has been cancelled
Test Integration / test-integration (6, 12) (push) Has been cancelled
Test Integration / test-integration (7, 12) (push) Has been cancelled
Test Integration / test-integration (8, 12) (push) Has been cancelled
Test Integration / test-integration (9, 12) (push) Has been cancelled
Test Integration / merge-test-results (push) Has been cancelled
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Build and Publish Documentation / Doc Process (push) Has been cancelled
Build experimental image on branch / build-webui (push) Has been cancelled
Build experimental image on branch / Build experimental image on branch (push) Has been cancelled
Test Integration / build (push) Has been cancelled
Test Unit / Unit tests (push) Has been cancelled
Test Unit / test-ui-unit (push) Has been cancelled
Test Integration / test-integration (0, 12) (push) Has been cancelled
Test Integration / test-integration (1, 12) (push) Has been cancelled
Test Integration / test-integration (10, 12) (push) Has been cancelled
Test Integration / test-integration (11, 12) (push) Has been cancelled
Test Integration / test-integration (2, 12) (push) Has been cancelled
Test Integration / test-integration (3, 12) (push) Has been cancelled
Test Integration / test-integration (4, 12) (push) Has been cancelled
Test Integration / test-integration (5, 12) (push) Has been cancelled
Test Integration / test-integration (6, 12) (push) Has been cancelled
Test Integration / test-integration (7, 12) (push) Has been cancelled
Test Integration / test-integration (8, 12) (push) Has been cancelled
Test Integration / test-integration (9, 12) (push) Has been cancelled
Test Integration / merge-test-results (push) Has been cancelled
This commit is contained in:
parent
1fbe532c64
commit
db21cbae1a
5 changed files with 188 additions and 2 deletions
|
|
@ -320,6 +320,17 @@ There are three types of filters:
|
|||
- **Extended:** Optional filters for Gateway controllers, such as `ResponseHeaderModifier` and `RequestMirror`.
|
||||
- **ExtensionRef:** Additional filters provided by the Gateway controller. In Traefik, these are the [HTTP middlewares](../http/middlewares/overview.md) supported through the [Middleware CRD](../kubernetes/crd/http/middleware.md).
|
||||
|
||||
!!! info "Supported Filter Types"
|
||||
|
||||
Traefik supports the following route-level filter types:
|
||||
|
||||
- `RequestHeaderModifier`: Add, set, or remove HTTP request headers before forwarding to the backend.
|
||||
- `ResponseHeaderModifier`: Add, set, or remove HTTP response headers.
|
||||
- `RequestRedirect`: Redirect the request to a different URL.
|
||||
- `URLRewrite`: Rewrite the request URL path and/or hostname.
|
||||
- `CORS`: Configure Cross-Origin Resource Sharing (CORS) response headers.
|
||||
- `ExtensionRef`: Reference a Traefik [Middleware](../kubernetes/crd/http/middleware.md) resource.
|
||||
|
||||
!!! info "ExtensionRef Filters"
|
||||
|
||||
To use Traefik middlewares as `ExtensionRef` filters, the Kubernetes CRD provider must be enabled in the static configuration, as detailed in the [documentation](../../install-configuration/providers/kubernetes/kubernetes-crd.md).
|
||||
|
|
@ -442,6 +453,7 @@ This allows request modifications to be applied to specific backends, enabling t
|
|||
- `ResponseHeaderModifier`: Add, set, or remove HTTP response headers.
|
||||
- `RequestRedirect`: Redirect the request to a different URL.
|
||||
- `URLRewrite`: Rewrite the request URL path and/or hostname.
|
||||
- `CORS`: Configure Cross-Origin Resource Sharing (CORS) response headers.
|
||||
- `ExtensionRef`: Reference a Traefik [Middleware](../kubernetes/crd/http/middleware.md) resource.
|
||||
|
||||
!!! info "Middlewares Execution Order"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ profiles:
|
|||
result: success
|
||||
statistics:
|
||||
Failed: 0
|
||||
Passed: 27
|
||||
Passed: 29
|
||||
Skipped: 0
|
||||
supportedFeatures:
|
||||
- BackendTLSPolicy
|
||||
|
|
@ -42,6 +42,7 @@ profiles:
|
|||
- HTTPRouteBackendProtocolH2C
|
||||
- HTTPRouteBackendProtocolWebSocket
|
||||
- HTTPRouteBackendRequestHeaderModification
|
||||
- HTTPRouteCORS
|
||||
- HTTPRouteDestinationPortMatching
|
||||
- HTTPRouteHostRewrite
|
||||
- HTTPRouteMethodMatching
|
||||
|
|
@ -63,7 +64,6 @@ profiles:
|
|||
- GatewayInfrastructurePropagation
|
||||
- GatewayStaticAddresses
|
||||
- HTTPRouteBackendTimeout
|
||||
- HTTPRouteCORS
|
||||
- HTTPRouteRequestMirror
|
||||
- HTTPRouteRequestMultipleMirrors
|
||||
- HTTPRouteRequestPercentageMirror
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] {
|
|||
features.HTTPRouteBackendRequestHeaderModificationFeature,
|
||||
features.HTTPRouteNamedRouteRule,
|
||||
features.HTTPRouteParentRefPortFeature,
|
||||
features.HTTPRouteCORS,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -407,6 +407,12 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, paren
|
|||
middleware,
|
||||
})
|
||||
|
||||
case gatev1.HTTPRouteFilterCORS:
|
||||
middlewares = append(middlewares, namedMiddleware{
|
||||
name,
|
||||
createCORS(filter.CORS),
|
||||
})
|
||||
|
||||
default:
|
||||
// As per the spec: https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
|
||||
// In all cases where incompatible or unsupported filters are
|
||||
|
|
@ -962,6 +968,52 @@ func createURLRewrite(filter *gatev1.HTTPURLRewriteFilter, pathMatch gatev1.HTTP
|
|||
}, nil
|
||||
}
|
||||
|
||||
func createCORS(filter *gatev1.HTTPCORSFilter) *dynamic.Middleware {
|
||||
var allowOrigins, allowOriginsRegex []string
|
||||
for _, origin := range filter.AllowOrigins {
|
||||
if prefix, suffix, found := strings.Cut(string(origin), "*"); found {
|
||||
switch {
|
||||
case prefix != "" || suffix != "":
|
||||
allowOriginsRegex = append(allowOriginsRegex, "^"+regexp.QuoteMeta(prefix)+`.*`+regexp.QuoteMeta(suffix)+"$")
|
||||
default:
|
||||
// Convert to regex so the middleware echoes the specific request origin rather than the literal "*".
|
||||
// The Gateway API conformance tests require the specific origin, even when AllowCredentials is false/unset.
|
||||
allowOriginsRegex = append(allowOriginsRegex, ".*")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
allowOrigins = append(allowOrigins, string(origin))
|
||||
}
|
||||
|
||||
var allowMethods []string
|
||||
for _, m := range filter.AllowMethods {
|
||||
allowMethods = append(allowMethods, string(m))
|
||||
}
|
||||
|
||||
var allowHeaders []string
|
||||
for _, h := range filter.AllowHeaders {
|
||||
allowHeaders = append(allowHeaders, string(h))
|
||||
}
|
||||
|
||||
var exposeHeaders []string
|
||||
for _, h := range filter.ExposeHeaders {
|
||||
exposeHeaders = append(exposeHeaders, string(h))
|
||||
}
|
||||
|
||||
return &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowCredentials: ptr.Deref(filter.AllowCredentials, false),
|
||||
AccessControlAllowOriginList: allowOrigins,
|
||||
AccessControlAllowOriginListRegex: allowOriginsRegex,
|
||||
AccessControlAllowMethods: allowMethods,
|
||||
AccessControlAllowHeaders: allowHeaders,
|
||||
AccessControlExposeHeaders: exposeHeaders,
|
||||
AccessControlMaxAge: ptr.To(int64(filter.MaxAge)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getHTTPServiceProtocol(portSpec corev1.ServicePort) (string, error) {
|
||||
if portSpec.Protocol != corev1.ProtocolTCP {
|
||||
return "", errors.New("only TCP protocol is supported")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"k8s.io/utils/ptr"
|
||||
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
|
@ -203,3 +204,123 @@ func Test_buildMatchRule(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createCORS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
filter *gatev1.HTTPCORSFilter
|
||||
expected *dynamic.Middleware
|
||||
}{
|
||||
{
|
||||
desc: "Empty filter",
|
||||
filter: &gatev1.HTTPCORSFilter{},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Plain origins",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com", "https://bar.example.com"},
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowOriginList: []string{"https://foo.example.com", "https://bar.example.com"},
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Wildcard origin becomes catch-all regex",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"*"},
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{`.*`},
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Origin with bare wildcard host",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"https://*"},
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{`^https://.*$`},
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Origin with wildcard subdomain becomes regex",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"https://*.example.com"},
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{`^https://.*\.example\.com$`},
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Mixed plain and wildcard origins",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com", "https://*.example.com"},
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowOriginList: []string{"https://foo.example.com"},
|
||||
AccessControlAllowOriginListRegex: []string{`^https://.*\.example\.com$`},
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "All fields set",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com"},
|
||||
AllowCredentials: ptr.To(true),
|
||||
AllowMethods: []gatev1.HTTPMethodWithWildcard{"GET", "POST"},
|
||||
AllowHeaders: []gatev1.HTTPHeaderName{"X-Foo", "X-Bar"},
|
||||
ExposeHeaders: []gatev1.HTTPHeaderName{"X-Baz"},
|
||||
MaxAge: 600,
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowCredentials: true,
|
||||
AccessControlAllowOriginList: []string{"https://foo.example.com"},
|
||||
AccessControlAllowMethods: []string{"GET", "POST"},
|
||||
AccessControlAllowHeaders: []string{"X-Foo", "X-Bar"},
|
||||
AccessControlExposeHeaders: []string{"X-Baz"},
|
||||
AccessControlMaxAge: ptr.To(int64(600)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "AllowCredentials explicitly false",
|
||||
filter: &gatev1.HTTPCORSFilter{
|
||||
AllowCredentials: ptr.To(false),
|
||||
},
|
||||
expected: &dynamic.Middleware{
|
||||
Headers: &dynamic.Headers{
|
||||
AccessControlAllowCredentials: false,
|
||||
AccessControlMaxAge: ptr.To(int64(0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, test.expected, createCORS(test.filter))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue