Skip to content

Commit

Permalink
use wildcard to match alloworigins
Browse files Browse the repository at this point in the history
Signed-off-by: huabing zhao <zhaohuabing@gmail.com>
  • Loading branch information
zhaohuabing committed Jan 3, 2024
1 parent b935d77 commit c3648da
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 95 deletions.
18 changes: 17 additions & 1 deletion api/v1alpha1/cors_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,27 @@ package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// Origin is defined by the scheme (protocol), hostname (domain), and port of
// the URL used to access it. The hostname can be “precise” which is just the
// domain name or “wildcard” which is a domain name prefixed with a single
// wildcard label such as “*.example.com”.
//
// For example, the following are valid origins:
// - https://foo.example.com
// - https://*.example.com
// - http://foo.example.com:8080
// - http://*.example.com:8080
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^https?:\/\/(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(:[0-9]+)?$`
type Origin string

// CORS defines the configuration for Cross-Origin Resource Sharing (CORS).
type CORS struct {
// AllowOrigins defines the origins that are allowed to make requests.
// +kubebuilder:validation:MinItems=1
AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins"`
AllowOrigins []Origin `json:"allowOrigins,omitempty" yaml:"allowOrigins"`
// AllowMethods defines the methods that are allowed to make requests.
// +kubebuilder:validation:MinItems=1
AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods"`
Expand Down
6 changes: 2 additions & 4 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,17 @@ spec:
description: AllowOrigins defines the origins that are allowed
to make requests.
items:
description: StringMatch defines how to match any strings. This
is a general purpose match condition that can be used by other
EG APIs that need to match against a string.
properties:
type:
default: Exact
description: Type specifies how to match against a string.
enum:
- Exact
- Prefix
- Suffix
- RegularExpression
type: string
value:
description: Value specifies the string value that the match
must have.
maxLength: 1024
minLength: 1
type: string
required:
- value
type: object
description: "Origin is defined by the scheme (protocol), hostname
(domain), and port of the URL used to access it. The hostname
can be “precise” which is just the domain name or “wildcard”
which is a domain name prefixed with a single wildcard label
such as “*.example.com”. \n For example, the following are
valid origins: - https://foo.example.com - https://*.example.com
- http://foo.example.com:8080 - http://*.example.com:8080"
maxLength: 253
minLength: 1
pattern: ^https?:\/\/(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(:[0-9]+)?$
type: string
minItems: 1
type: array
exposeHeaders:
Expand Down
55 changes: 20 additions & 35 deletions internal/gatewayapi/securitypolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/ir"
"github.com/envoyproxy/gateway/internal/status"
"github.com/envoyproxy/gateway/internal/utils/regex"
)

func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy,
Expand Down Expand Up @@ -269,9 +268,7 @@ func (t *Translator) translateSecurityPolicyForRoute(
)

if policy.Spec.CORS != nil {
if cors, err = t.buildCORS(policy.Spec.CORS); err != nil {
errs = multierror.Append(errs, err)
}
cors = t.buildCORS(policy.Spec.CORS)
}

if policy.Spec.JWT != nil {
Expand Down Expand Up @@ -325,10 +322,7 @@ func (t *Translator) translateSecurityPolicyForGateway(
)

if policy.Spec.CORS != nil {
cors, err = t.buildCORS(policy.Spec.CORS)
if err != nil {
errs = multierror.Append(errs, err)
}
cors = t.buildCORS(policy.Spec.CORS)
}

if policy.Spec.JWT != nil {
Expand Down Expand Up @@ -377,38 +371,19 @@ func (t *Translator) translateSecurityPolicyForGateway(
return errs
}

func (t *Translator) buildCORS(cors *egv1a1.CORS) (*ir.CORS, error) {
func (t *Translator) buildCORS(cors *egv1a1.CORS) *ir.CORS {
var allowOrigins []*ir.StringMatch

for _, origin := range cors.AllowOrigins {
origin := origin.DeepCopy()

// matchType default to exact
matchType := egv1a1.StringMatchExact
if origin.Type != nil {
matchType = *origin.Type
}

// TODO zhaohuabing: extract a utils function to build StringMatch
switch matchType {
case egv1a1.StringMatchExact:
allowOrigins = append(allowOrigins, &ir.StringMatch{
Exact: &origin.Value,
})
case egv1a1.StringMatchPrefix:
origin := origin
if isWildcard(string(origin)) {
regexStr := wildcard2regex(string(origin))
allowOrigins = append(allowOrigins, &ir.StringMatch{
Prefix: &origin.Value,
SafeRegex: &regexStr,
})
case egv1a1.StringMatchSuffix:
} else {
allowOrigins = append(allowOrigins, &ir.StringMatch{
Suffix: &origin.Value,
})
case egv1a1.StringMatchRegularExpression:
if err := regex.Validate(origin.Value); err != nil {
return nil, err
}
allowOrigins = append(allowOrigins, &ir.StringMatch{
SafeRegex: &origin.Value,
Exact: (*string)(&origin),
})
}
}
Expand All @@ -420,7 +395,17 @@ func (t *Translator) buildCORS(cors *egv1a1.CORS) (*ir.CORS, error) {
ExposeHeaders: cors.ExposeHeaders,
MaxAge: cors.MaxAge,
AllowCredentials: cors.AllowCredentials != nil && *cors.AllowCredentials,
}, nil
}
}

func isWildcard(s string) bool {
return strings.ContainsAny(s, "*")
}

func wildcard2regex(wildcard string) string {
regexStr := strings.ReplaceAll(wildcard, ".", "\\.")
regexStr = strings.ReplaceAll(regexStr, "*", ".*")
return regexStr
}

func (t *Translator) buildJWT(jwt *egv1a1.JWT) *ir.JWT {
Expand Down
76 changes: 76 additions & 0 deletions internal/gatewayapi/securitypolicy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 gatewayapi

import (
"regexp"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_wildcard2regex(t *testing.T) {
tests := []struct {
name string
wildcard string
origin string
want int
}{
{
name: "test1",
wildcard: "http://*.example.com",
origin: "http://foo.example.com",
want: 1,
},
{
name: "test2",
wildcard: "http://*.example.com",
origin: "http://foo.bar.example.com",
want: 1,
},
{
name: "test3",
wildcard: "http://*.example.com",
origin: "http://foo.bar.com",
want: 0,
},
{
name: "test4",
wildcard: "http://*.example.com",
origin: "https://foo.example.com",
want: 0,
},
{
name: "test5",
wildcard: "http://*.example.com:8080",
origin: "http://foo.example.com:8080",
want: 1,
},
{
name: "test6",
wildcard: "http://*.example.com:8080",
origin: "http://foo.bar.example.com:8080",
want: 1,
},
{
name: "test7",
wildcard: "http://*.example.com:8080",
origin: "http://foo.example.com",
want: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
regexStr := wildcard2regex(tt.wildcard)
regex, err := regexp.Compile(regexStr)
require.Nil(t, err)
finds := regex.FindAllString(tt.origin, -1)
assert.Equalf(t, tt.want, len(finds), "wildcard2regex(%v)", tt.wildcard)
})
}
}
12 changes: 4 additions & 8 deletions internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ securityPolicies:
namespace: envoy-gateway
cors:
allowOrigins:
- type: RegularExpression
value: "FooBar[0-9]+"
- type: Exact
value: foo.bar.com
- "http://*.example.com"
- "http://foo.bar.com"
allowMethods:
- GET
- POST
Expand All @@ -103,10 +101,8 @@ securityPolicies:
namespace: default
cors:
allowOrigins:
- type: Prefix
value: example
- type: Suffix
value: bar.org
- "https://*.test.com:8080"
- "https://www.test.org:8080"
allowMethods:
- GET
- POST
Expand Down
20 changes: 8 additions & 12 deletions internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,8 @@ securityPolicies:
- GET
- POST
allowOrigins:
- type: Prefix
value: example
- type: Suffix
value: bar.org
- https://*.test.com:8080
- https://www.test.org:8080
exposeHeaders:
- x-header-7
- x-header-8
Expand Down Expand Up @@ -234,10 +232,8 @@ securityPolicies:
- GET
- POST
allowOrigins:
- type: RegularExpression
value: FooBar[0-9]+
- type: Exact
value: foo.bar.com
- http://*.example.com
- http://foo.bar.com
exposeHeaders:
- x-header-3
- x-header-4
Expand Down Expand Up @@ -280,9 +276,9 @@ xdsIR:
allowOrigins:
- distinct: false
name: ""
safeRegex: FooBar[0-9]+
safeRegex: http://.*\.example\.com
- distinct: false
exact: foo.bar.com
exact: http://foo.bar.com
name: ""
exposeHeaders:
- x-header-3
Expand Down Expand Up @@ -324,10 +320,10 @@ xdsIR:
allowOrigins:
- distinct: false
name: ""
prefix: example
safeRegex: https://.*\.test\.com:8080
- distinct: false
exact: https://www.test.org:8080
name: ""
suffix: bar.org
exposeHeaders:
- x-header-7
- x-header-8
Expand Down
3 changes: 1 addition & 2 deletions site/content/en/latest/api/extension_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ _Appears in:_

| Field | Description |
| --- | --- |
| `allowOrigins` _[StringMatch](#stringmatch) array_ | AllowOrigins defines the origins that are allowed to make requests. |
| `allowOrigins` _Origin array_ | AllowOrigins defines the origins that are allowed to make requests. |
| `allowMethods` _string array_ | AllowMethods defines the methods that are allowed to make requests. |
| `allowHeaders` _string array_ | AllowHeaders defines the headers that are allowed to be sent with requests. |
| `exposeHeaders` _string array_ | ExposeHeaders defines the headers that can be exposed in the responses. |
Expand Down Expand Up @@ -1833,7 +1833,6 @@ _Appears in:_
StringMatch defines how to match any strings. This is a general purpose match condition that can be used by other EG APIs that need to match against a string.

_Appears in:_
- [CORS](#cors)
- [ProxyMetrics](#proxymetrics)

| Field | Description |
Expand Down
21 changes: 17 additions & 4 deletions site/content/en/latest/user/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ spec:
name: backend
cors:
allowOrigins:
- type: RegularExpression
value: .*\.foo\.com
- "http://*.foo.com"
- "http://*.foo.com:80"
allowMethods:
- GET
- POST
Expand Down Expand Up @@ -84,16 +84,29 @@ You should see the below response, indicating that the request from `http://www.
If you try to send a request from `http://www.bar.com`, you should see the below response:

```shell
curl -H "Origin: http://www.bar.com" \
curl -H "Origin: http://www.foo.com" \
-H "Host: www.example.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS -v -s \
http://$GATEWAY_HOST \
1> /dev/null
1>
```

You won't see any CORS headers in the response, indicating that the request from `http://www.bar.com` was not allowed.

If you try to send a request from a different port, you should also see similar response because the port number is not
included in the allowed origins.

```shell
```shell
curl -H "Origin: http://www.foo.com:8080" \
-H "Host: www.example.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS -v -s \
http://$GATEWAY_HOST \
1> /dev/null
```

Note: CORS specification requires that the browsers to send a preflight request to the server to ask if it's allowed
to access the limited resource in another domains. The browsers are supposed to follow the response from the server to
determine whether to send the actual request or not. The CORS filter only response to the preflight requests according to
Expand Down
Loading

0 comments on commit c3648da

Please sign in to comment.