diff --git a/Makefile b/Makefile index 18296b3e..f5892f36 100644 --- a/Makefile +++ b/Makefile @@ -289,7 +289,7 @@ deploy-cert-manager: $(KUBECTL) wait --timeout=5m -n cert-manager deployment/cert-manager-cainjector --for=condition=Available .PHONY: deploy-controller -deploy-controller: kubectl +deploy-controller: kind kubectl cd controller/ && KIND=$(KIND) KIND_OPTION="-n htnn" KUBECTL=$(KUBECTL) make deploy $(KUBECTL) wait --timeout=5m -n controller-system deployment/controller-controller-manager --for=condition=Available diff --git a/controller/Makefile b/controller/Makefile index 4121cc61..ec38dea0 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -151,7 +151,7 @@ deploy: deploy-image manifests kustomize ## Deploy controller to the K8s cluster $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - .PHONY: undeploy -undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - ##@ Build Dependencies diff --git a/controller/internal/translation/testdata/plugins/cors.in.yml b/controller/internal/translation/testdata/plugins/cors.in.yml new file mode 100644 index 00000000..d043384f --- /dev/null +++ b/controller/internal/translation/testdata/plugins/cors.in.yml @@ -0,0 +1,17 @@ +apiVersion: mosn.io/v1 +kind: HTTPFilterPolicy +metadata: + name: policy + namespace: default +spec: + targetRef: + group: networking.istio.io + kind: VirtualService + name: default + filters: + cors: + config: + allowOriginStringMatch: + - safeRegex: + regex: .*\.envoyproxy\.io + allowMethods: "GET" diff --git a/controller/internal/translation/testdata/plugins/cors.out.yml b/controller/internal/translation/testdata/plugins/cors.out.yml new file mode 100644 index 00000000..67df0b86 --- /dev/null +++ b/controller/internal/translation/testdata/plugins/cors.out.yml @@ -0,0 +1,46 @@ +- metadata: + creationTimestamp: null + name: htnn-h-default.local + spec: + configPatches: + - applyTo: HTTP_ROUTE + match: + routeConfiguration: + vhost: + name: default.local:80 + route: + name: default/default + patch: + operation: MERGE + value: + typed_per_filter_config: + htnn.filters.http.cors: + '@type': type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy + allowMethods: GET + allowOriginStringMatch: + - safeRegex: + regex: .*\.envoyproxy\.io + status: {} +- metadata: + creationTimestamp: null + name: htnn-http-filter + spec: + configPatches: + - applyTo: HTTP_FILTER + match: + context: GATEWAY + listener: + filterChain: + filter: + name: envoy.filters.network.http_connection_manager + subFilter: + name: htnn.filters.http.golang + patch: + operation: INSERT_BEFORE + value: + disabled: true + name: htnn.filters.http.cors + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors + priority: 100 + status: {} diff --git a/controller/plugins/cors/config.go b/controller/plugins/cors/config.go new file mode 100644 index 00000000..d4c675aa --- /dev/null +++ b/controller/plugins/cors/config.go @@ -0,0 +1,61 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cors + +import ( + cors "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" + + "mosn.io/htnn/pkg/filtermanager/api" + "mosn.io/htnn/pkg/plugins" +) + +const ( + Name = "cors" +) + +func init() { + plugins.RegisterHttpPlugin(Name, &plugin{}) +} + +type plugin struct { + plugins.PluginMethodDefaultImpl +} + +func (p *plugin) Type() plugins.PluginType { + return plugins.TypeSecurity +} + +func (p *plugin) Order() plugins.PluginOrder { + return plugins.PluginOrder{ + Position: plugins.OrderPositionOuter, + Operation: plugins.OrderOperationInsertLast, + } +} + +func (p *plugin) Config() api.PluginConfig { + return &cors.CorsPolicy{} +} + +func (p *plugin) RouteConfigTypeURL() string { + return "type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy" +} + +func (p *plugin) HTTPFilterConfigPlaceholder() map[string]interface{} { + return map[string]interface{}{ + "typed_config": map[string]interface{}{ + "@type": "type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors", + }, + } +} diff --git a/controller/plugins/plugins.go b/controller/plugins/plugins.go index fa97f31a..4e26efb4 100644 --- a/controller/plugins/plugins.go +++ b/controller/plugins/plugins.go @@ -17,6 +17,7 @@ package plugins import ( _ "mosn.io/htnn/controller/plugins/bandwidth_limit" _ "mosn.io/htnn/controller/plugins/buffer" + _ "mosn.io/htnn/controller/plugins/cors" _ "mosn.io/htnn/controller/plugins/fault" _ "mosn.io/htnn/controller/plugins/local_ratelimit" _ "mosn.io/htnn/controller/plugins/lua" diff --git a/e2e/pkg/suite/suite.go b/e2e/pkg/suite/suite.go index 13734725..cfdcffd7 100644 --- a/e2e/pkg/suite/suite.go +++ b/e2e/pkg/suite/suite.go @@ -153,6 +153,10 @@ func (suite *Suite) Head(path string, header http.Header) (*http.Response, error return suite.do("HEAD", path, header, nil) } +func (suite *Suite) Options(path string, header http.Header) (*http.Response, error) { + return suite.do("OPTIONS", path, header, nil) +} + func (suite *Suite) Get(path string, header http.Header) (*http.Response, error) { return suite.do("GET", path, header, nil) } diff --git a/e2e/tests/cors.go b/e2e/tests/cors.go new file mode 100644 index 00000000..d939d902 --- /dev/null +++ b/e2e/tests/cors.go @@ -0,0 +1,46 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "mosn.io/htnn/e2e/pkg/suite" +) + +func init() { + suite.Register(suite.Test{ + Manifests: []string{"base/httproute.yml"}, + Run: func(t *testing.T, suite *suite.Suite) { + hdr := http.Header{} + origin := "http://x.default.local" + hdr.Set("Origin", origin) + meth := "POST" + hdr.Set("Access-Control-Request-Method", meth) + rsp, err := suite.Options("/echo", hdr) + require.NoError(t, err) + require.Equal(t, 200, rsp.StatusCode) + require.Equal(t, origin, rsp.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, meth, rsp.Header.Get("Access-Control-Allow-Methods")) + rsp, _ = suite.Post("/echo", hdr, strings.NewReader("")) + require.Equal(t, 200, rsp.StatusCode) + require.Equal(t, origin, rsp.Header.Get("Access-Control-Allow-Origin")) + }, + }) +} diff --git a/e2e/tests/cors.yml b/e2e/tests/cors.yml new file mode 100644 index 00000000..c595dc45 --- /dev/null +++ b/e2e/tests/cors.yml @@ -0,0 +1,16 @@ +apiVersion: mosn.io/v1 +kind: HTTPFilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: default + filters: + cors: + config: + allowOriginStringMatch: + - safeRegex: + regex: .*\.default\.local + allowMethods: POST diff --git a/site/content/en/docs/reference/plugins/cors.md b/site/content/en/docs/reference/plugins/cors.md new file mode 100644 index 00000000..4f48b152 --- /dev/null +++ b/site/content/en/docs/reference/plugins/cors.md @@ -0,0 +1,78 @@ +--- +title: CORS +--- + +## Description + +The `cors` plugin handles Cross-Origin Resource Sharing requests by leveraging Envoy's `cors` filter. + +## Attribute + +| | | +|-------|----------| +| Type | Security | +| Order | Outer | + +## Configuration + +See the corresponding [Envoy documentation](https://www.envoyproxy.io/docs/envoy/v1.28.0/configuration/http/http_filters/cors_filter). + +## Usage + +Assuming we have the following HTTPRoute attached to `localhost:10000`, with a backend server listening on port `8080`: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: default +spec: + parentRefs: + - name: default + namespace: default + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend + port: 8080 +``` + +By applying the configuration below, Cross-Origin Resource Sharing requests sent to `http://localhost:10000/` will be processed. If the `Origin` header of an OPTIONS request matches the regular expression `.*\.default\.local`, then the corresponding response will include the configured `Access-Control-Allow-*` response headers. + +```yaml +apiVersion: mosn.io/v1 +kind: HTTPFilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: default + filters: + cors: + config: + allowOriginStringMatch: + - safeRegex: + regex: ".*\.default\.local" + allowMethods: POST +``` + +Let's try it out: + +``` +$ curl -X OPTIONS http://localhost:10000/ -H "Origin: https://x.efault.local" -H "Access-Control-Request-Method: GET" -i +HTTP/1.1 200 OK +server: istio-envoy +... + +$ curl -X OPTIONS http://localhost:10000/ -H "Origin: https://x.default.local" -H "Access-Control-Request-Method: GET" -i +HTTP/1.1 200 OK +access-control-allow-origin: https://x.default.local +access-control-allow-methods: POST +server: istio-envoy +... +``` diff --git a/site/content/zh-hans/docs/reference/plugins/cors.md b/site/content/zh-hans/docs/reference/plugins/cors.md new file mode 100644 index 00000000..b091a0d2 --- /dev/null +++ b/site/content/zh-hans/docs/reference/plugins/cors.md @@ -0,0 +1,78 @@ +--- +title: CORS +--- + +## 说明 + +`cors` 插件通过利用 Envoy 的 `cors` 过滤器处理跨域资源共享的请求。 + +## 属性 + +| | | +|-------|----------| +| Type | Security | +| Order | Outer | + +## 配置 + +请参阅相应的 [Envoy 文档](https://www.envoyproxy.io/docs/envoy/v1.28.0/configuration/http/http_filters/cors_filter)。 + +## 用法 + +假设我们有下面附加到 `localhost:10000` 的 HTTPRoute,并且有一个后端服务器监听端口 `8080`: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: default +spec: + parentRefs: + - name: default + namespace: default + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend + port: 8080 +``` + +通过应用下面的配置,发送到 `http://localhost:10000/` 的处理跨域资源共享的请求将会被处理。如果 OPTIONS 请求的请求头的 `Origin` 匹配到正则表达式 `.*\.default\.local`,那么对应的响应中会有配置的 `Access-Control-Allow-*` 响应头。 + +```yaml +apiVersion: mosn.io/v1 +kind: HTTPFilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: default + filters: + cors: + config: + allowOriginStringMatch: + - safeRegex: + regex: ".*\.default\.local" + allowMethods: POST +``` + +让我们试一下: + +``` +$ curl -X OPTIONS http://localhost:10000/ -H "Origin: https://x.efault.local" -H "Access-Control-Request-Method: GET" -i +HTTP/1.1 200 OK +server: istio-envoy +... + +$ curl -X OPTIONS http://localhost:10000/ -H "Origin: https://x.default.local" -H "Access-Control-Request-Method: GET" -i +HTTP/1.1 200 OK +access-control-allow-origin: https://x.default.local +access-control-allow-methods: POST +server: istio-envoy +... +```