Compare commits

...

11 Commits

Author SHA1 Message Date
Kent Dong
51d7124454 release: Prepare for releasing v1.0.1 (#367) 2023-06-16 18:23:17 +08:00
Hinsteny Hisoka
ec6a185adc feat: support only build one plugin to testing specified WasmPlugin (#371)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2023-06-15 15:21:54 +08:00
Hinsteny Hisoka
f9ffda288b feature: e2e framework supports testing wasm plugins (#369) 2023-06-08 11:00:30 +08:00
Kent Dong
2dbe41324a fix: Fix some compatibility issues related to older versions of K8s (#363) 2023-06-06 18:02:47 +08:00
Qianglin Li
80d6ecfddb feat: support full path regex annotation (#286)
Signed-off-by: charlie <qianglin98@qq.com>
2023-06-02 18:56:22 +08:00
Ink33
b23fae7a12 ci: enable building files caching (#323)
Signed-off-by: Ink33 <Ink33@smlk.org>
Co-authored-by: Xunzhuo <bitliu@tencent.com>
2023-06-01 11:35:37 +08:00
WeixinX
2c19d97252 feat: Add e2e testcases for 'auth-tls-secret' and 'ssl-cipher' (#354) 2023-05-30 11:08:31 +08:00
Xunzhuo
efd7ccd5fe fix: e2e refers (#345) 2023-05-26 12:02:39 +08:00
Xunzhuo
81fd0d6386 fix: e2e refers (#345) 2023-05-26 12:01:50 +08:00
Jun
176ddc6963 Plugin cors (#349) 2023-05-25 19:21:21 +08:00
WeixinX
44637c2449 fix: Fix e2e 'request header control' and add some testcases (#352) 2023-05-25 15:51:39 +08:00
49 changed files with 2981 additions and 150 deletions

View File

@@ -23,6 +23,28 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Setup Golang Caches
uses: actions/cache@v3
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
- run: git stash # restore patch
# test
- name: Run Coverage Tests
run: GOPROXY="https://proxy.golang.org,direct" make go.test.coverage
@@ -38,15 +60,37 @@ jobs:
runs-on: ubuntu-latest
needs: [lint,coverage-test]
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: "Setup Go"
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: "checkout ${{ github.ref }}"
uses: actions/checkout@v3
- name: Setup Golang Caches
uses: actions/cache@v3
with:
fetch-depth: 2
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
- run: git stash # restore patch
- name: "Build Higress Binary"
run: GOPROXY="https://proxy.golang.org,direct" make build
@@ -67,16 +111,78 @@ jobs:
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v3
- name: "Setup Go"
uses: actions/setup-go@v3
with:
go-version: 1.19
- uses: actions/checkout@v3
- name: Setup Golang Caches
uses: actions/cache@v3
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
- run: git stash # restore patch
- name: "Run Ingress Conformance Tests"
run: GOPROXY="https://proxy.golang.org,direct" make ingress-conformance-test
ingress-wasmplugin-test:
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v3
- name: "Setup Go"
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
- run: git stash # restore patch
- name: "Run Ingress WasmPlugins Tests"
run: GOPROXY="https://proxy.golang.org,direct" make ingress-wasmplugin-test
publish:
runs-on: ubuntu-latest
needs: [ingress-conformance-test,gateway-conformance-test]
needs: [ingress-conformance-test,gateway-conformance-test,ingress-wasmplugin-test]
steps:
- uses: actions/checkout@v3

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ bazel-out
bazel-testlogs
bazel-wasm-cpp
tools/bin/
helm/**/charts/**.tgz
helm/**/charts/**.tgz
tools/hack/cluster.conf

View File

@@ -128,6 +128,9 @@ build-gateway: prebuild external/package/envoy.tar.gz
build-istio: prebuild
cd external/istio; rm -rf out; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=amd64 BUILD_WITH_CONTAINER=1 DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
build-wasmplugins:
./tools/hack/build-wasm-plugins.sh
pre-install:
cp api/kubernetes/customresourcedefinitions.gen.yaml helm/core/crds
@@ -145,6 +148,9 @@ ISTIO_LATEST_IMAGE_TAG ?= 1.0.0
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
install-dev-wasmplugin: build-wasmplugins pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'global.volumeWasmPlugins=true'
uninstall:
helm uninstall higress -n higress-system
@@ -197,6 +203,10 @@ gateway-conformance-test:
.PHONY: ingress-conformance-test
ingress-conformance-test: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev run-ingress-e2e-test delete-cluster
# ingress-wasmplugin-test runs ingress wasmplugin tests.
.PHONY: ingress-wasmplugin-test
ingress-wasmplugin-test: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin run-ingress-e2e-test-wasmplugin delete-cluster
# create-cluster creates a kube cluster with kind.
.PHONY: create-cluster
create-cluster: $(tools/kind)
@@ -221,3 +231,13 @@ run-ingress-e2e-test:
@echo -e "\n\033[36mWaiting higress-gateway to be ready...\033[0m\n"
kubectl wait --timeout=10m -n higress-system deployment/higress-gateway --for=condition=Available
go test -v -tags conformance ./test/ingress/e2e_test.go --ingress-class=higress --debug=true
# run-ingress-e2e-test starts to run ingress e2e tests.
.PHONY: run-ingress-e2e-test-wasmplugin
run-ingress-e2e-test-wasmplugin:
@echo -e "\n\033[36mRunning higress conformance tests...\033[0m"
@echo -e "\n\033[36mWaiting higress-controller to be ready...\033[0m\n"
kubectl wait --timeout=10m -n higress-system deployment/higress-controller --for=condition=Available
@echo -e "\n\033[36mWaiting higress-gateway to be ready...\033[0m\n"
kubectl wait --timeout=10m -n higress-system deployment/higress-gateway --for=condition=Available
go test -v -tags conformance ./test/ingress/e2e_test.go -isWasmPluginTest=true --ingress-class=higress --debug=true

View File

@@ -1 +1 @@
v1.0.0
v1.0.1

1
helm/core/.helmignore Normal file
View File

@@ -0,0 +1 @@
crds/customresourcedefinitions.gen_lt1.16.yaml

View File

@@ -0,0 +1,176 @@
# DO NOT EDIT - Generated by Cue OpenAPI generator based on Istio APIs.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
"helm.sh/resource-policy": keep
name: wasmplugins.extensions.higress.io
spec:
group: extensions.higress.io
names:
categories:
- higress-io
- extensions-higress-io
kind: WasmPlugin
listKind: WasmPluginList
plural: wasmplugins
singular: wasmplugin
scope: Namespaced
additionalPrinterColumns:
- description: 'CreationTimestamp is a timestamp representing the server time
when this object was created. It is not guaranteed to be set in happens-before
order across separate operations. Clients may not set this value. It is represented
in RFC3339 form and is in UTC. Populated by the system. Read-only. Null for
lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata'
JSONPath: .metadata.creationTimestamp
name: Age
type: date
versions:
- name: v1alpha1
served: true
storage: true
version: v1alpha1
validation:
openAPIV3Schema:
properties:
spec:
properties:
defaultConfig:
type: object
x-kubernetes-preserve-unknown-fields: true
defaultConfigDisable:
type: boolean
imagePullPolicy:
description: The pull behaviour to be applied when fetching an OCI
image.
enum:
- UNSPECIFIED_POLICY
- IfNotPresent
- Always
type: string
imagePullSecret:
description: Credentials to use for OCI image pulling.
type: string
matchRules:
items:
properties:
config:
type: object
x-kubernetes-preserve-unknown-fields: true
configDisable:
type: boolean
domain:
items:
type: string
type: array
ingress:
items:
type: string
type: array
type: object
type: array
phase:
description: Determines where in the filter chain this `WasmPlugin`
is to be injected.
enum:
- UNSPECIFIED_PHASE
- AUTHN
- AUTHZ
- STATS
type: string
pluginConfig:
description: The configuration that will be passed on to the plugin.
type: object
x-kubernetes-preserve-unknown-fields: true
pluginName:
type: string
priority:
description: Determines ordering of `WasmPlugins` in the same `phase`.
nullable: true
type: integer
sha256:
description: SHA256 checksum that will be used to verify Wasm module
or OCI container.
type: string
url:
description: URL of a Wasm module or OCI container.
type: string
verificationKey:
type: string
type: object
status:
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
"helm.sh/resource-policy": keep
name: mcpbridges.networking.higress.io
spec:
group: networking.higress.io
names:
categories:
- higress-io
kind: McpBridge
listKind: McpBridgeList
plural: mcpbridges
singular: mcpbridge
scope: Namespaced
versions:
- name: v1
served: true
storage: true
version: v1
validation:
openAPIV3Schema:
properties:
spec:
properties:
registries:
items:
properties:
consulNamespace:
type: string
domain:
type: string
nacosAccessKey:
type: string
nacosAddressServer:
type: string
nacosGroups:
items:
type: string
type: array
nacosNamespace:
type: string
nacosNamespaceId:
type: string
nacosRefreshInterval:
format: int64
type: integer
nacosSecretKey:
type: string
name:
type: string
port:
type: integer
type:
type: string
zkServicesPath:
items:
type: string
type: array
type: object
type: array
type: object
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
subresources:
status: {}
---

View File

@@ -100,6 +100,10 @@ spec:
fieldPath: spec.serviceAccountName
- name: KUBECONFIG
value: /var/run/secrets/remote/config
- name: PRIORITIZED_LEADER_ELECTION
value: "false"
- name: INJECT_ENABLED
value: "false"
{{- if .Values.pilot.env }}
{{- range $key, $val := .Values.pilot.env }}
- name: {{ $key }}

View File

@@ -206,6 +206,10 @@ spec:
- mountPath: /etc/istio/custom-bootstrap
name: custom-bootstrap-volume
{{- end }}
{{- if .Values.global.volumeWasmPlugins }}
- mountPath: /opt/plugins
name: local-wasmplugins-volume
{{- end }}
{{- if .Values.gateway.hostNetwork }}
hostNetwork: {{ .Values.gateway.hostNetwork }}
dnsPolicy: ClusterFirstWithHostNet
@@ -274,3 +278,9 @@ spec:
containerName: higress-gateway
divisor: 1m
resource: limits.cpu
{{- if .Values.global.volumeWasmPlugins }}
- name: local-wasmplugins-volume
hostPath:
path: /opt/plugins
type: Directory
{{- end }}

View File

@@ -4,6 +4,6 @@ dependencies:
version: 1.0.0
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.0.0
digest: sha256:fb0f1b6816df5f5ac6888a93fb148b55b669affc9ac99336dd1ac818b8f84ace
generated: "2023-05-22T17:42:02.506864+08:00"
version: 1.0.1
digest: sha256:cb0808ac6feff2bebf1184969defe5d0b7bf6d12d45e9bd39751df94af731dc0
generated: "2023-06-16T10:15:54.5326712+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.0.0
appVersion: 1.0.1
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -15,6 +15,6 @@ dependencies:
version: 1.0.0
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.0.0
version: 1.0.1
type: application
version: 1.0.0
version: 1.0.1

View File

@@ -76,7 +76,15 @@ func (i *Ingress) NeedRegexMatch() bool {
return false
}
return i.Rewrite.RewriteTarget != "" || i.Rewrite.UseRegex
return i.Rewrite.RewriteTarget != "" || i.IsPrefixRegexMatch() || i.IsFullPathRegexMatch()
}
func (i *Ingress) IsPrefixRegexMatch() bool {
return i.Rewrite.UseRegex
}
func (i *Ingress) IsFullPathRegexMatch() bool {
return i.Rewrite.FullPathRegex
}
func (i *Ingress) IsCanary() bool {

View File

@@ -25,6 +25,7 @@ const (
rewritePath = "rewrite-path"
rewriteTarget = "rewrite-target"
useRegex = "use-regex"
fullPathRegex = "full-path-regex"
upstreamVhost = "upstream-vhost"
re2Regex = "\\$[0-9]"
@@ -38,6 +39,7 @@ var (
type RewriteConfig struct {
RewriteTarget string
UseRegex bool
FullPathRegex bool
RewriteHost string
RewritePath string
}
@@ -52,13 +54,16 @@ func (r rewrite) Parse(annotations Annotations, config *Ingress, _ *GlobalContex
rewriteConfig := &RewriteConfig{}
rewriteConfig.RewriteTarget, _ = annotations.ParseStringASAP(rewriteTarget)
rewriteConfig.UseRegex, _ = annotations.ParseBoolASAP(useRegex)
rewriteConfig.FullPathRegex, _ = annotations.ParseBoolForHigress(fullPathRegex)
rewriteConfig.RewriteHost, _ = annotations.ParseStringASAP(upstreamVhost)
rewriteConfig.RewritePath, _ = annotations.ParseStringForHigress(rewritePath)
if rewriteConfig.RewritePath == "" && rewriteConfig.RewriteTarget != "" {
// When rewrite target is present and not empty,
// we will enforce regex match on all rules in this ingress.
rewriteConfig.UseRegex = true
if !rewriteConfig.UseRegex && !rewriteConfig.FullPathRegex {
rewriteConfig.UseRegex = true
}
// We should convert nginx regex rule to envoy regex rule.
rewriteConfig.RewriteTarget = convertToRE2(rewriteConfig.RewriteTarget)
@@ -108,12 +113,13 @@ func convertToRE2(target string) string {
func NeedRegexMatch(annotations map[string]string) bool {
target, _ := Annotations(annotations).ParseStringASAP(rewriteTarget)
regex, _ := Annotations(annotations).ParseBoolASAP(useRegex)
useRegex, _ := Annotations(annotations).ParseBoolASAP(useRegex)
fullPathRegex, _ := Annotations(annotations).ParseBoolForHigress(fullPathRegex)
return regex || target != ""
return useRegex || target != "" || fullPathRegex
}
func needRewriteConfig(annotations Annotations) bool {
return annotations.HasASAP(rewriteTarget) || annotations.HasASAP(useRegex) ||
annotations.HasASAP(upstreamVhost) || annotations.HasHigress(rewritePath)
annotations.HasASAP(upstreamVhost) || annotations.HasHigress(rewritePath) || annotations.HasHigress(fullPathRegex)
}

View File

@@ -59,7 +59,11 @@ const (
Prefix PathType = "prefix"
Regex PathType = "regex"
// PrefixRegex :if PathType is PrefixRegex, then the /foo/bar/[A-Z0-9]{3} is actually ^/foo/bar/[A-Z0-9]{3}.*
PrefixRegex PathType = "prefixRegex"
// FullPathRegex :if PathType is FullPathRegex, then the /foo/bar/[A-Z0-9]{3} is actually ^/foo/bar/[A-Z0-9]{3}$
FullPathRegex PathType = "fullPathRegex"
DefaultStatusUpdateInterval = 10 * time.Second

View File

@@ -43,11 +43,11 @@ func TestConstructRouteName(t *testing.T) {
{
input: &WrapperHTTPRoute{
Host: "*.test.com",
OriginPathType: Regex,
OriginPathType: PrefixRegex,
OriginPath: "/test/(.*)/?[0-9]",
HTTPRoute: &networking.HTTPRoute{},
},
expect: "*.test.com-regex-/test/(.*)/?[0-9]",
expect: "*.test.com-prefixRegex-/test/(.*)/?[0-9]",
},
{
input: &WrapperHTTPRoute{
@@ -390,7 +390,7 @@ func TestSortRoutes(t *testing.T) {
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Regex,
OriginPathType: PrefixRegex,
OriginPath: "/d(.*)",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{

View File

@@ -534,8 +534,12 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
var pathType common.PathType
originPath := httpPath.Path
if wrapper.AnnotationsConfig.NeedRegexMatch() {
pathType = common.Regex
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
}
} else {
switch *httpPath.PathType {
case ingress.PathTypeExact:
@@ -741,8 +745,12 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
var pathType common.PathType
originPath := httpPath.Path
if wrapper.AnnotationsConfig.NeedRegexMatch() {
pathType = common.Regex
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
}
} else {
switch *httpPath.PathType {
case ingress.PathTypeExact:
@@ -1166,10 +1174,14 @@ func (c *controller) generateHttpMatches(pathType common.PathType, path string,
httpMatch := &networking.HTTPMatchRequest{}
switch pathType {
case common.Regex:
case common.PrefixRegex:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: path + ".*"},
}
case common.FullPathRegex:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: path + "$"},
}
case common.Exact:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{Exact: path},

View File

@@ -39,8 +39,7 @@ type statusSyncer struct {
watchedNamespace string
ingressLister ingresslister.IngressLister
ingressClassLister ingresslister.IngressClassLister
ingressLister ingresslister.IngressLister
// search service in the mse vpc
serviceLister listerv1.ServiceLister
}
@@ -48,11 +47,10 @@ type statusSyncer struct {
// newStatusSyncer creates a new instance
func newStatusSyncer(localKubeClient, client kubelib.Client, controller *controller, namespace string) *statusSyncer {
return &statusSyncer{
client: client,
controller: controller,
watchedNamespace: namespace,
ingressLister: client.KubeInformer().Networking().V1beta1().Ingresses().Lister(),
ingressClassLister: client.KubeInformer().Networking().V1beta1().IngressClasses().Lister(),
client: client,
controller: controller,
watchedNamespace: namespace,
ingressLister: client.KubeInformer().Networking().V1beta1().Ingresses().Lister(),
// search service in the mse vpc
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
}

View File

@@ -517,8 +517,12 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
var pathType common.PathType
originPath := httpPath.Path
if wrapper.AnnotationsConfig.NeedRegexMatch() {
pathType = common.Regex
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
}
} else {
switch *httpPath.PathType {
case ingress.PathTypeExact:
@@ -610,10 +614,14 @@ func (c *controller) generateHttpMatches(pathType common.PathType, path string,
httpMatch := &networking.HTTPMatchRequest{}
switch pathType {
case common.Regex:
case common.PrefixRegex:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: path + ".*"},
}
case common.FullPathRegex:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: path + "$"},
}
case common.Exact:
httpMatch.Uri = &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{Exact: path},
@@ -747,8 +755,12 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
var pathType common.PathType
originPath := httpPath.Path
if wrapper.AnnotationsConfig.NeedRegexMatch() {
pathType = common.Regex
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
}
} else {
switch *httpPath.PathType {
case ingress.PathTypeExact:

View File

@@ -40,8 +40,7 @@ type statusSyncer struct {
watchedNamespace string
ingressLister ingresslister.IngressLister
ingressClassLister ingresslister.IngressClassLister
ingressLister ingresslister.IngressLister
// search service in the mse vpc
serviceLister listerv1.ServiceLister
}
@@ -49,11 +48,10 @@ type statusSyncer struct {
// newStatusSyncer creates a new instance
func newStatusSyncer(localKubeClient, client kubelib.Client, controller *controller, namespace string) *statusSyncer {
return &statusSyncer{
client: client,
controller: controller,
watchedNamespace: namespace,
ingressLister: client.KubeInformer().Networking().V1().Ingresses().Lister(),
ingressClassLister: client.KubeInformer().Networking().V1().IngressClasses().Lister(),
client: client,
controller: controller,
watchedNamespace: namespace,
ingressLister: client.KubeInformer().Networking().V1().Ingresses().Lister(),
// search service in the mse vpc
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
}

View File

@@ -0,0 +1,230 @@
# 功能说明
`cors` 插件可以为服务端启用 CORSCross-Origin Resource Sharing跨域资源共享的返回 http 响应头。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|-----------------------|-----------------|-------|---------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| allow_origins | array of string | 选填 | * | 允许跨域访问的 Origin格式为 scheme://host:port示例如 http://example.com:8081。当 allow_credentials 为 false 时,可以使用 * 来表示允许所有 Origin 通过 |
| allow_origin_patterns | array of string | 选填 | - | 允许跨域访问的 Origin 模式匹配, 用 * 匹配域名或者端口, <br/>比如 http://*.example.com -- 匹配域名, http://*.example.com:[8080,9090] -- 匹配域名和指定端口, http://*.example.com:[*] -- 匹配域名和所有端口。单独 * 表示匹配所有域名和端口 |
| allow_methods | array of string | 选填 | GET, PUT, POST, DELETE, PATCH, OPTIONS | 允许跨域访问的 Method比如GETPOST 等。可以使用 * 来表示允许所有 Method。 |
| allow_headers | array of string | 选填 | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 |
| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 |
| allow_credentials | bool | 选填 | false | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 true在 allow_origins 不能使用 * 替换成使用 allow_origin_patterns * |
| max_age | number | 选填 | 86400秒 | 浏览器缓存 CORS 结果的最大时间,单位为秒。<br/>在这个时间范围内,浏览器会复用上一次的检查结果 |
> 注意
> * allow_credentials 是一个很敏感的选项请谨慎开启。开启之后allow_credentials 和 allow_origins 为 * 不能同时使用,同时设置时, allow_origins 值为 "*" 生效。
> * allow_origins 和 allow_origin_patterns 可以同时设置, 先检查 allow_origins 是否匹配,然后再检查 allow_origin_patterns 是否匹配
> * 非法 CORS 请求, HTTP 状态码返回是 403 返回体内容为 "Invalid CORS request"
# 配置示例
## 允许所有跨域访问, 不允许请求方携带凭据
```yaml
allow_origins:
- '*'
allow_methods:
- '*'
allow_headers:
- '*'
expose_headers:
- '*'
allow_credentials: false
max_age: 7200
```
## 允许所有跨域访问,同时允许请求方携带凭据
```yaml
allow_origin_patterns:
- '*'
allow_methods:
- '*'
allow_headers:
- '*'
expose_headers:
- '*'
allow_credentials: true
max_age: 7200
```
## 允许特定子域,特定方法,特定请求头跨域访问,同时允许请求方携带凭据
```yaml
allow_origin_patterns:
- http://*.example.com
- http://*.example.org:[8080,9090]
allow_methods:
- GET
- PUT
- POST
- DELETE
allow_headers:
- Token
- Content-Type
- Authorization
expose_headers:
- '*'
allow_credentials: true
max_age: 7200
```
# 测试
## 测试配置
```yaml
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: mcp-cors-httpbin
namespace: higress-system
spec:
registries:
- domain: httpbin.org
name: httpbin
port: 80
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: httpbin.dns
higress.io/upstream-vhost: "httpbin.org"
higress.io/backend-protocol: HTTP
name: ingress-cors-httpbin
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: httpbin.example.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: mcp-cors-httpbin
path: /
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: wasm-cors-httpbin
namespace: higress-system
spec:
defaultConfigDisable: true
matchRules:
- config:
allow_origins:
- http://httpbin.example.net
allow_origin_patterns:
- http://*.example.com:[*]
- http://*.example.org:[9090,8080]
allow_methods:
- GET
- POST
- PATCH
allow_headers:
- Content-Type
- Token
- Authorization
expose_headers:
- X-Custom-Header
- X-Env-UTM
allow_credentials: true
max_age: 3600
configDisable: false
ingress:
- ingress-cors-httpbin
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0
imagePullPolicy: Always
```
## 请求测试
### 简单请求
```shell
curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
> x-cors-version: 1.0.0
> access-control-allow-origin: http://httpbin2.example.org:9090
> access-control-expose-headers: X-Custom-Header,X-Env-UTM
> access-control-allow-credentials: true
```
### 预检请求
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Token" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
< x-cors-version: 1.0.0
< access-control-allow-origin: http://httpbin2.example.org:9090
< access-control-allow-methods: GET,POST,PATCH
< access-control-allow-headers: Content-Type,Token,Authorization
< access-control-expose-headers: X-Custom-Header,X-Env-UTM
< access-control-allow-credentials: true
< access-control-max-age: 3600
< date: Tue, 23 May 2023 11:41:28 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0
```
### 非法 CORS Origin 预检请求
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" http://127.0.0.1/anything/get\?foo\=1
HTTP/1.1 403 Forbidden
< content-length: 70
< content-type: text/plain
< x-cors-version: 1.0.0
< date: Tue, 23 May 2023 11:27:01 GMT
< server: istio-envoy
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
### 非法 CORS Method 预检请求
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: DELETE" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 49
< content-type: text/plain
< x-cors-version: 1.0.0
< date: Tue, 23 May 2023 11:28:51 GMT
< server: istio-envoy
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
### 非法 CORS Header 预检请求
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: TokenView" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 52
< content-type: text/plain
< x-cors-version: 1.0.0
< date: Tue, 23 May 2023 11:31:03 GMT
< server: istio-envoy
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
# 参考文档
- https://www.ruanyifeng.com/blog/2016/04/cors.html
- https://fetch.spec.whatwg.org/#http-cors-protocol

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,457 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 config
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
)
const (
defaultMatchAll = "*"
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultAllAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS, HEAD, TRACE, CONNECT"
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
"If-Modified-Since,Cache-Control,Content-Type,Authorization"
defaultMaxAge = 86400
protocolHttpName = "http"
protocolHttpPort = "80"
protocolHttpsName = "https"
protocolHttpsPort = "443"
HeaderPluginDebug = "X-Cors-Version"
HeaderPluginTrace = "X-Cors-Trace"
HeaderOrigin = "Origin"
HttpMethodOptions = "OPTIONS"
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods"
HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials"
HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
HeaderAccessControlMaxAge = "Access-Control-Max-Age"
HeaderControlRequestMethod = "Access-Control-Request-Method"
HeaderControlRequestHeaders = "Access-Control-Request-Headers"
HttpContextKey = "CORS"
)
var portsRegex = regexp.MustCompile(`(.*):\[(\*|\d+(,\d+)*)]`)
type OriginPattern struct {
declaredPattern string
pattern *regexp.Regexp
patternValue string
}
func newOriginPatternFromString(declaredPattern string) OriginPattern {
declaredPattern = strings.ToLower(strings.TrimSuffix(declaredPattern, "/"))
matches := portsRegex.FindAllStringSubmatch(declaredPattern, -1)
portList := ""
patternValue := declaredPattern
if len(matches) > 0 {
patternValue = matches[0][1]
portList = matches[0][2]
}
patternValue = "\\Q" + patternValue + "\\E"
patternValue = strings.ReplaceAll(patternValue, "*", "\\E.*\\Q")
if len(portList) > 0 {
if portList == defaultMatchAll {
patternValue += "(:\\d+)?"
} else {
patternValue += ":(" + strings.ReplaceAll(portList, ",", "|") + ")"
}
}
return OriginPattern{
declaredPattern: declaredPattern,
patternValue: patternValue,
pattern: regexp.MustCompile(patternValue),
}
}
type CorsConfig struct {
// allowOrigins A list of origins for which cross-origin requests are allowed.
// Be a specific domain, e.g. "https://example.com", or the CORS defined special value "*" for all origins.
// Keep in mind however that the CORS spec does not allow "*" when allowCredentials is set to true, using allowOriginPatterns instead
// By default, it is set to "*" when allowOriginPatterns is not set too.
allowOrigins []string
// allowOriginPatterns A list of origin patterns for which cross-origin requests are allowed
// origins patterns with "*" anywhere in the host name in addition to port
// lists Examples:
// https://*.example.com -- domains ending with example.com
// https://*.example.com:[8080,9090] -- domains ending with example.com on port 8080 or port 9090
// https://*.example.com:[*] -- domains ending with example.com on any port, including the default port
// The special value "*" allows all origins
// By default, it is not set.
allowOriginPatterns []OriginPattern
// allowMethods A list of method for which cross-origin requests are allowed
// The special value "*" allows all methods.
// By default, it is set to "GET, PUT, POST, DELETE, PATCH, OPTIONS".
allowMethods []string
// allowHeaders A list of headers that a pre-flight request can list as allowed
// The special value "*" allows actual requests to send any header
// By default, it is set to "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
allowHeaders []string
// exposeHeaders A list of response headers an actual response might have and can be exposed.
// The special value "*" allows all headers to be exposed for non-credentialed requests.
// By default, it is not set
exposeHeaders []string
// allowCredentials Whether user credentials are supported.
// By default, it is not set (i.e. user credentials are not supported).
allowCredentials bool
// maxAge Configure how long, in seconds, the response from a pre-flight request can be cached by clients.
// By default, it is set to 86400 seconds.
maxAge int
}
type HttpCorsContext struct {
IsValid bool
ValidReason string
IsPreFlight bool
IsCorsRequest bool
AllowOrigin string
AllowMethods string
AllowHeaders string
ExposeHeaders string
AllowCredentials bool
MaxAge int
}
func (c *CorsConfig) GetVersion() string {
return "1.0.0"
}
func (c *CorsConfig) FillDefaultValues() {
if len(c.allowOrigins) == 0 && len(c.allowOriginPatterns) == 0 && c.allowCredentials == false {
c.allowOrigins = []string{defaultMatchAll}
}
if len(c.allowHeaders) == 0 {
c.allowHeaders = []string{defaultAllowHeaders}
}
if len(c.allowMethods) == 0 {
c.allowMethods = strings.Split(defaultAllowMethods, "")
}
if c.maxAge == 0 {
c.maxAge = defaultMaxAge
}
}
func (c *CorsConfig) AddAllowOrigin(origin string) error {
origin = strings.TrimSpace(origin)
if len(origin) == 0 {
return nil
}
if origin == defaultMatchAll {
if c.allowCredentials == true {
return errors.New("can't set origin to * when allowCredentials is true, use AllowOriginPatterns instead")
}
c.allowOrigins = []string{defaultMatchAll}
return nil
}
c.allowOrigins = append(c.allowOrigins, strings.TrimSuffix(origin, "/"))
return nil
}
func (c *CorsConfig) AddAllowHeader(header string) {
header = strings.TrimSpace(header)
if len(header) == 0 {
return
}
if header == defaultMatchAll {
c.allowHeaders = []string{defaultMatchAll}
return
}
c.allowHeaders = append(c.allowHeaders, header)
}
func (c *CorsConfig) AddAllowMethod(method string) {
method = strings.TrimSpace(method)
if len(method) == 0 {
return
}
if method == defaultMatchAll {
c.allowMethods = []string{defaultMatchAll}
return
}
c.allowMethods = append(c.allowMethods, strings.ToUpper(method))
}
func (c *CorsConfig) AddExposeHeader(header string) {
header = strings.TrimSpace(header)
if len(header) == 0 {
return
}
if header == defaultMatchAll {
c.exposeHeaders = []string{defaultMatchAll}
return
}
c.exposeHeaders = append(c.exposeHeaders, header)
}
func (c *CorsConfig) AddAllowOriginPattern(pattern string) {
pattern = strings.TrimSpace(pattern)
if len(pattern) == 0 {
return
}
originPattern := newOriginPatternFromString(pattern)
c.allowOriginPatterns = append(c.allowOriginPatterns, originPattern)
}
func (c *CorsConfig) SetAllowCredentials(allowCredentials bool) error {
if allowCredentials && len(c.allowOrigins) > 0 && c.allowOrigins[0] == defaultMatchAll {
return errors.New("can't set allowCredentials to true when allowOrigin is *")
}
c.allowCredentials = allowCredentials
return nil
}
func (c *CorsConfig) SetMaxAge(maxAge int) {
if maxAge <= 0 {
c.maxAge = defaultMaxAge
} else {
c.maxAge = maxAge
}
}
func (c *CorsConfig) Process(scheme string, host string, method string, headers [][2]string) (HttpCorsContext, error) {
scheme = strings.ToLower(strings.TrimSpace(scheme))
host = strings.ToLower(strings.TrimSpace(host))
method = strings.ToLower(strings.TrimSpace(method))
// Init httpCorsContext with default values
httpCorsContext := HttpCorsContext{IsValid: true, IsPreFlight: false, IsCorsRequest: false, AllowCredentials: false, MaxAge: 0}
// Get request origin, controlRequestMethod, controlRequestHeaders from http headers
origin := ""
controlRequestMethod := ""
controlRequestHeaders := ""
for _, header := range headers {
key := header[0]
// Get origin
if strings.ToLower(key) == strings.ToLower(HeaderOrigin) {
origin = strings.TrimSuffix(strings.TrimSpace(header[1]), "/")
}
// Get control request method & headers
if strings.ToLower(key) == strings.ToLower(HeaderControlRequestMethod) {
controlRequestMethod = strings.TrimSpace(header[1])
}
if strings.ToLower(key) == strings.ToLower(HeaderControlRequestHeaders) {
controlRequestHeaders = strings.TrimSpace(header[1])
}
}
// Parse if request is CORS and pre-flight request.
isCorsRequest := c.isCorsRequest(scheme, host, origin)
isPreFlight := c.isPreFlight(origin, method, controlRequestMethod)
httpCorsContext.IsCorsRequest = isCorsRequest
httpCorsContext.IsPreFlight = isPreFlight
// Skip when it is not CORS request
if !isCorsRequest {
httpCorsContext.IsValid = true
return httpCorsContext, nil
}
// Check origin
allowOrigin, originOk := c.checkOrigin(origin)
if !originOk {
// Reject: origin is not allowed
httpCorsContext.IsValid = false
httpCorsContext.ValidReason = fmt.Sprintf("origin:%s is not allowed", origin)
return httpCorsContext, nil
}
// Check method
requestMethod := method
if isPreFlight {
requestMethod = controlRequestMethod
}
allowMethods, methodOk := c.checkMethods(requestMethod)
if !methodOk {
// Reject: method is not allowed
httpCorsContext.IsValid = false
httpCorsContext.ValidReason = fmt.Sprintf("method:%s is not allowed", requestMethod)
return httpCorsContext, nil
}
// Check headers
allowHeaders, headerOK := c.checkHeaders(controlRequestHeaders)
if isPreFlight && !headerOK {
// Reject: headers are not allowed
httpCorsContext.IsValid = false
httpCorsContext.ValidReason = "Reject: headers are not allowed"
return httpCorsContext, nil
}
// Store result in httpCorsContext and return it.
httpCorsContext.AllowOrigin = allowOrigin
if isPreFlight {
httpCorsContext.AllowMethods = allowMethods
}
if isPreFlight && len(allowHeaders) > 0 {
httpCorsContext.AllowHeaders = allowHeaders
}
if isPreFlight && c.maxAge > 0 {
httpCorsContext.MaxAge = c.maxAge
}
if len(c.exposeHeaders) > 0 {
httpCorsContext.ExposeHeaders = strings.Join(c.exposeHeaders, ",")
}
httpCorsContext.AllowCredentials = c.allowCredentials
return httpCorsContext, nil
}
func (c *CorsConfig) checkOrigin(origin string) (string, bool) {
origin = strings.TrimSpace(origin)
if len(origin) == 0 {
return "", false
}
matchOrigin := strings.ToLower(origin)
// Check exact match
for _, allowOrigin := range c.allowOrigins {
if allowOrigin == defaultMatchAll {
return origin, true
}
if strings.ToLower(allowOrigin) == matchOrigin {
return origin, true
}
}
// Check pattern match
for _, allowOriginPattern := range c.allowOriginPatterns {
if allowOriginPattern.declaredPattern == defaultMatchAll || allowOriginPattern.pattern.MatchString(matchOrigin) {
return origin, true
}
}
return "", false
}
func (c *CorsConfig) checkHeaders(requestHeaders string) (string, bool) {
if len(c.allowHeaders) == 0 {
return "", false
}
if len(requestHeaders) == 0 {
return strings.Join(c.allowHeaders, ","), true
}
// Return all request headers when allowHeaders contains *
if c.allowHeaders[0] == defaultMatchAll {
return requestHeaders, true
}
checkHeaders := strings.Split(requestHeaders, ",")
// Each request header should be existed in allowHeaders configuration
for _, h := range checkHeaders {
isExist := false
for _, allowHeader := range c.allowHeaders {
if strings.ToLower(h) == strings.ToLower(allowHeader) {
isExist = true
break
}
}
if !isExist {
return "", false
}
}
return strings.Join(c.allowHeaders, ","), true
}
func (c *CorsConfig) checkMethods(requestMethod string) (string, bool) {
if len(requestMethod) == 0 {
return "", false
}
// Find method existed in allowMethods configuration
for _, method := range c.allowMethods {
if method == defaultMatchAll {
return defaultAllAllowMethods, true
}
if strings.ToLower(method) == strings.ToLower(requestMethod) {
return strings.Join(c.allowMethods, ","), true
}
}
return "", false
}
func (c *CorsConfig) isPreFlight(origin, method, controllerRequestMethod string) bool {
return len(origin) > 0 && strings.ToLower(method) == strings.ToLower(HttpMethodOptions) && len(controllerRequestMethod) > 0
}
func (c *CorsConfig) isCorsRequest(scheme, host, origin string) bool {
if len(origin) == 0 {
return false
}
url, err := url.Parse(strings.TrimSpace(origin))
if err != nil {
return false
}
// Check scheme
if strings.ToLower(scheme) != strings.ToLower(url.Scheme) {
return true
}
// Check host and port
port := ""
originPort := ""
originHost := ""
host, port = c.getHostAndPort(scheme, host)
originHost, originPort = c.getHostAndPort(url.Scheme, url.Host)
if host != originHost || port != originPort {
return true
}
return false
}
func (c *CorsConfig) getHostAndPort(scheme string, host string) (string, string) {
// Get host and port
scheme = strings.ToLower(scheme)
host = strings.ToLower(host)
port := ""
hosts := strings.Split(host, ":")
if len(hosts) > 1 {
host = hosts[0]
port = hosts[1]
}
// Get default port according scheme
if len(port) == 0 && scheme == protocolHttpName {
port = protocolHttpPort
}
if len(port) == 0 && scheme == protocolHttpsName {
port = protocolHttpsPort
}
return host, port
}

View File

@@ -0,0 +1,408 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 config
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCorsConfig_getHostAndPort(t *testing.T) {
tests := []struct {
name string
scheme string
host string
wantHost string
wantPort string
}{
{
name: "http without port",
scheme: "http",
host: "http.example.com",
wantHost: "http.example.com",
wantPort: "80",
},
{
name: "https without port",
scheme: "https",
host: "http.example.com",
wantHost: "http.example.com",
wantPort: "443",
},
{
name: "http with port and case insensitive",
scheme: "hTTp",
host: "hTTp.Example.com:8080",
wantHost: "http.example.com",
wantPort: "8080",
},
{
name: "https with port and case insensitive",
scheme: "hTTps",
host: "hTTp.Example.com:8080",
wantHost: "http.example.com",
wantPort: "8080",
},
{
name: "protocal is not http",
scheme: "wss",
host: "hTTp.Example.com",
wantHost: "http.example.com",
wantPort: "",
},
{
name: "protocal is not http",
scheme: "wss",
host: "hTTp.Example.com:8080",
wantHost: "http.example.com",
wantPort: "8080",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{}
host, port := c.getHostAndPort(tt.scheme, tt.host)
assert.Equal(t, tt.wantHost, host)
assert.Equal(t, tt.wantPort, port)
})
}
}
func TestCorsConfig_isCorsRequest(t *testing.T) {
tests := []struct {
name string
scheme string
host string
origin string
want bool
}{
{
name: "blank origin",
scheme: "http",
host: "httpbin.example.com",
origin: "",
want: false,
},
{
name: "normal equal case with space and case ",
scheme: "http",
host: "httpbin.example.com",
origin: "http://hTTPbin.Example.com",
want: false,
},
{
name: "cors request with port diff",
scheme: "http",
host: "httpbin.example.com",
origin: " http://httpbin.example.com:8080 ",
want: true,
},
{
name: "cors request with scheme diff",
scheme: "http",
host: "httpbin.example.com",
origin: " https://HTTPpbin.Example.com ",
want: true,
},
{
name: "cors request with host diff",
scheme: "http",
host: "httpbin.example.com",
origin: " http://HTTPpbin.Example.org ",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{}
assert.Equalf(t, tt.want, c.isCorsRequest(tt.scheme, tt.host, tt.origin), "isCorsRequest(%v, %v, %v)", tt.scheme, tt.host, tt.origin)
})
}
}
func TestCorsConfig_isPreFlight(t *testing.T) {
tests := []struct {
name string
origin string
method string
controllerRequestMethod string
want bool
}{
{
name: "blank case",
origin: "",
method: "",
controllerRequestMethod: "",
want: false,
},
{
name: "normal case",
origin: "http://httpbin.example.com",
method: "Options",
controllerRequestMethod: "PUT",
want: true,
},
{
name: "bad case with diff method",
origin: "http://httpbin.example.com",
method: "GET",
controllerRequestMethod: "PUT",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{}
assert.Equalf(t, tt.want, c.isPreFlight(tt.origin, tt.method, tt.controllerRequestMethod), "isPreFlight(%v, %v, %v)", tt.origin, tt.method, tt.controllerRequestMethod)
})
}
}
func TestCorsConfig_checkMethods(t *testing.T) {
tests := []struct {
name string
allowMethods []string
requestMethod string
wantMethods string
wantOk bool
}{
{
name: "default *",
allowMethods: []string{"*"},
requestMethod: "GET",
wantMethods: defaultAllAllowMethods,
wantOk: true,
},
{
name: "normal allow case",
allowMethods: []string{"GET", "PUT", "HEAD"},
requestMethod: "get",
wantMethods: "GET,PUT,HEAD",
wantOk: true,
},
{
name: "forbidden case",
allowMethods: []string{"GET", "PUT", "HEAD"},
requestMethod: "POST",
wantMethods: "",
wantOk: false,
},
{
name: "blank method",
allowMethods: []string{"GET", "PUT", "HEAD"},
requestMethod: "",
wantMethods: "",
wantOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{
allowMethods: tt.allowMethods,
}
allowMethods, allowOk := c.checkMethods(tt.requestMethod)
assert.Equalf(t, tt.wantMethods, allowMethods, "checkMethods(%v)", tt.requestMethod)
assert.Equalf(t, tt.wantOk, allowOk, "checkMethods(%v)", tt.requestMethod)
})
}
}
func TestCorsConfig_checkHeaders(t *testing.T) {
tests := []struct {
name string
allowHeaders []string
requestHeaders string
wantHeaders string
wantOk bool
}{
{
name: "not pre-flight",
allowHeaders: []string{"Content-Type", "Authorization"},
requestHeaders: "",
wantHeaders: "Content-Type,Authorization",
wantOk: true,
},
{
name: "blank allowheaders case 1",
allowHeaders: []string{},
requestHeaders: "",
wantHeaders: "",
wantOk: false,
},
{
name: "blank allowheaders case 2",
requestHeaders: "Authorization",
wantHeaders: "",
wantOk: false,
},
{
name: "allowheaders *",
allowHeaders: []string{"*"},
requestHeaders: "Content-Type,Authorization",
wantHeaders: "Content-Type,Authorization",
wantOk: true,
},
{
name: "allowheader values 1",
allowHeaders: []string{"Content-Type", "Authorization"},
requestHeaders: "Content-Type,Authorization",
wantHeaders: "Content-Type,Authorization",
wantOk: true,
},
{
name: "allowheader values 2",
allowHeaders: []string{"Content-Type", "Authorization"},
requestHeaders: "",
wantHeaders: "Content-Type,Authorization",
wantOk: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{
allowHeaders: tt.allowHeaders,
}
allowHeaders, allowOk := c.checkHeaders(tt.requestHeaders)
assert.Equalf(t, tt.wantHeaders, allowHeaders, "checkHeaders(%v)", tt.requestHeaders)
assert.Equalf(t, tt.wantOk, allowOk, "checkHeaders(%v)", tt.requestHeaders)
})
}
}
func TestCorsConfig_checkOrigin(t *testing.T) {
tests := []struct {
name string
allowOrigins []string
allowOriginPatterns []OriginPattern
origin string
wantOrigin string
wantOk bool
}{
{
name: "allowOrigins *",
allowOrigins: []string{defaultMatchAll},
allowOriginPatterns: []OriginPattern{},
origin: "http://Httpbin.Example.COM",
wantOrigin: "http://Httpbin.Example.COM",
wantOk: true,
},
{
name: "allowOrigins exact match case 1",
allowOrigins: []string{"http://httpbin.example.com"},
allowOriginPatterns: []OriginPattern{},
origin: "http://HTTPBin.EXample.COM",
wantOrigin: "http://HTTPBin.EXample.COM",
wantOk: true,
},
{
name: "allowOrigins exact match case 2",
allowOrigins: []string{"https://httpbin.example.com"},
allowOriginPatterns: []OriginPattern{},
origin: "http://HTTPBin.EXample.COM",
wantOrigin: "",
wantOk: false,
},
{
name: "OriginPattern pattern match with *",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("*"),
},
origin: "http://HTTPBin.EXample.COM",
wantOrigin: "http://HTTPBin.EXample.COM",
wantOk: true,
},
{
name: "OriginPattern pattern match case with any port",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[*]"),
},
origin: "http://HTTPBin.EXample.COM",
wantOrigin: "http://HTTPBin.EXample.COM",
wantOk: true,
},
{
name: "OriginPattern pattern match case with any port",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[*]"),
},
origin: "http://HTTPBin.EXample.COM:10000",
wantOrigin: "http://HTTPBin.EXample.COM:10000",
wantOk: true,
},
{
name: "OriginPattern pattern match case with specail port 1",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[8080,9090]"),
},
origin: "http://HTTPBin.EXample.COM:10000",
wantOrigin: "",
wantOk: false,
},
{
name: "OriginPattern pattern match case with specail port 2",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[8080,9090]"),
},
origin: "http://HTTPBin.EXample.COM:9090",
wantOrigin: "http://HTTPBin.EXample.COM:9090",
wantOk: true,
},
{
name: "OriginPattern pattern match case with specail port 3",
allowOrigins: []string{},
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[8080,9090]"),
},
origin: "http://HTTPBin.EXample.org:9090",
wantOrigin: "",
wantOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{
allowOrigins: tt.allowOrigins,
allowOriginPatterns: tt.allowOriginPatterns,
}
allowOrigin, allowOk := c.checkOrigin(tt.origin)
assert.Equalf(t, tt.wantOrigin, allowOrigin, "checkOrigin(%v)", tt.origin)
assert.Equalf(t, tt.wantOk, allowOk, "checkOrigin(%v)", tt.origin)
})
}
}

View File

@@ -0,0 +1,67 @@
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: mcp-cors-httpbin
namespace: higress-system
spec:
registries:
- domain: httpbin.org
name: httpbin
port: 80
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: httpbin.dns
higress.io/upstream-vhost: "httpbin.org"
higress.io/backend-protocol: HTTP
name: ingress-cors-httpbin
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: httpbin.example.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: mcp-cors-httpbin
path: /
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: wasm-cors-httpbin
namespace: higress-system
spec:
defaultConfigDisable: true
matchRules:
- config:
allow_origins:
- http://httpbin.example.net
allow_origin_patterns:
- http://*.example.com:[*]
- http://*.example.org:[9090,8080]
allow_methods:
- GET
- POST
- PATCH
allow_headers:
- Content-Type
- Token
- Authorization
expose_headers:
- X-Custom-Header
- X-Env-UTM
allow_credentials: true
max_age: 3600
configDisable: false
ingress:
- ingress-cors-httpbin
url: oci://docker.io/2456868764/cors:1.0.0
imagePullPolicy: Always

View File

@@ -0,0 +1,78 @@
static_resources:
listeners:
- name: main
address:
socket_address:
address: 0.0.0.0
port_value: 18000
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "httpbin.example.com"
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: |-
{
"allow_origins": ["http://httpbin.example.net"],
"allow_origin_patterns": ["http://*.example.com:[*]", "http://*.example.org:[9090,8080]"],
"allow_methods": ["GET","PUT","POST", "PATCH", "HEAD", "OPTIONS"],
"allow_credentials": true,
"allow_headers":["Content-Type", "Token","Authorization"],
"expose_headers":["X-Custom-Header"],
"max_age": 3600
}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "./main.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: httpbin
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
dns_refresh_rate: 5s
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin.org
port_value: 80
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001

View File

@@ -0,0 +1,21 @@
module cors
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230519024024-625c06e58f91
github.com/stretchr/testify v1.8.3
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
github.com/tidwall/gjson v1.14.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,20 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,169 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 main
import (
"cors/config"
"fmt"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
"cors",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
)
}
func parseConfig(json gjson.Result, corsConfig *config.CorsConfig, log wrapper.Log) error {
log.Debug("parseConfig()")
allowOrigins := json.Get("allow_origins").Array()
for _, origin := range allowOrigins {
if err := corsConfig.AddAllowOrigin(origin.String()); err != nil {
log.Warnf("failed to AddAllowOrigin:%s, error:%v", origin, err)
}
}
allowOriginPatterns := json.Get("allow_origin_patterns").Array()
for _, pattern := range allowOriginPatterns {
corsConfig.AddAllowOriginPattern(pattern.String())
}
allowMethods := json.Get("allow_methods").Array()
for _, method := range allowMethods {
corsConfig.AddAllowMethod(method.String())
}
allowHeaders := json.Get("allow_headers").Array()
for _, header := range allowHeaders {
corsConfig.AddAllowHeader(header.String())
}
exposeHeaders := json.Get("expose_headers").Array()
for _, header := range exposeHeaders {
corsConfig.AddExposeHeader(header.String())
}
allowCredentials := json.Get("allow_credentials").Bool()
if err := corsConfig.SetAllowCredentials(allowCredentials); err != nil {
log.Warnf("failed to set AllowCredentials error: %v", err)
}
maxAge := json.Get("max_age").Int()
corsConfig.SetMaxAge(int(maxAge))
// Fill default values
corsConfig.FillDefaultValues()
log.Debugf("corsConfig:%+v", corsConfig)
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig, log wrapper.Log) types.Action {
log.Debug("onHttpRequestHeaders()")
requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
method, _ := proxywasm.GetHttpRequestHeader(":method")
host := ctx.Host()
scheme := ctx.Scheme()
log.Debugf("scheme:%s, host:%s, method:%s, request:%s", scheme, host, method, requestUrl)
// Get headers
headers, _ := proxywasm.GetHttpRequestHeaders()
// Process request
httpCorsContext, err := corsConfig.Process(scheme, host, method, headers)
if err != nil {
log.Warnf("failed to process %s : %v", requestUrl, err)
return types.ActionContinue
}
log.Debugf("Process httpCorsContext:%+v", httpCorsContext)
// Set HttpContext
ctx.SetContext(config.HttpContextKey, httpCorsContext)
// Response forbidden when it is not valid cors request
if !httpCorsContext.IsValid {
headers := make([][2]string, 0)
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
proxywasm.SendHttpResponse(403, headers, []byte("Invalid CORS request"), -1)
return types.ActionPause
}
// Response directly when it is cors preflight request
if httpCorsContext.IsPreFlight {
headers := make([][2]string, 0)
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
proxywasm.SendHttpResponse(200, headers, nil, -1)
return types.ActionPause
}
return types.ActionContinue
}
func onHttpRequestBody(ctx wrapper.HttpContext, corsConfig config.CorsConfig, body []byte, log wrapper.Log) types.Action {
log.Debug("onHttpRequestBody()")
return types.ActionContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig, log wrapper.Log) types.Action {
log.Debug("onHttpResponseHeaders()")
// Remove trace header if existed
proxywasm.RemoveHttpResponseHeader(config.HeaderPluginTrace)
// Remove upstream cors response headers if existed
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowOrigin)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowMethods)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlExposeHeaders)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowCredentials)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlMaxAge)
// Add debug header
proxywasm.AddHttpResponseHeader(config.HeaderPluginDebug, corsConfig.GetVersion())
// Restore httpCorsContext from HttpContext
httpCorsContext, ok := ctx.GetContext(config.HttpContextKey).(config.HttpCorsContext)
if !ok {
log.Debug("restore httpCorsContext from HttpContext error")
return types.ActionContinue
}
log.Debugf("Restore httpCorsContext:%+v", httpCorsContext)
// Skip which it is not valid or not cors request
if !httpCorsContext.IsValid || !httpCorsContext.IsCorsRequest {
return types.ActionContinue
}
// Add Cors headers when it is cors and valid request
if len(httpCorsContext.AllowOrigin) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowOrigin, httpCorsContext.AllowOrigin)
}
if len(httpCorsContext.AllowMethods) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowMethods, httpCorsContext.AllowMethods)
}
if len(httpCorsContext.AllowHeaders) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowHeaders, httpCorsContext.AllowHeaders)
}
if len(httpCorsContext.ExposeHeaders) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlExposeHeaders, httpCorsContext.ExposeHeaders)
}
if httpCorsContext.AllowCredentials {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowCredentials, "true")
}
if httpCorsContext.MaxAge > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlMaxAge, fmt.Sprintf("%d", httpCorsContext.MaxAge))
}
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, corsConfig config.CorsConfig, body []byte, log wrapper.Log) types.Action {
log.Debug("onHttpResponseBody()")
return types.ActionContinue
}

View File

@@ -15,7 +15,13 @@ Higress e2e tests are mainly focusing on two parts for now:
![ingress-workflow](./ingress/pipeline.png)
Higress provides make target to run ingress api conformance tests: `make ingress-conformance-test`. It can be divided into below steps:
Higress provides make target to run ingress api conformance tests and wasmplugin tests,
+ API Tests: `make ingress-conformance-test`
+ WasmPlugin Tests: `make ingress-wasmplugin-test`
+ Only build one WasmPlugin for testing: `PLUGIN_NAME=request-block make ingress-wasmplugin-test`
It can be divided into below steps:
1. delete-cluster: checks if we have undeleted kind cluster.
2. create-cluster: create a new kind cluster.

View File

@@ -0,0 +1,206 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 (
"crypto/tls"
"testing"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/alibaba/higress/test/ingress/conformance/utils/cert"
"github.com/alibaba/higress/test/ingress/conformance/utils/http"
"github.com/alibaba/higress/test/ingress/conformance/utils/kubernetes"
"github.com/alibaba/higress/test/ingress/conformance/utils/suite"
)
func init() {
HigressConformanceTests = append(HigressConformanceTests, HTTPRouteDownstreamEncryption)
}
var HTTPRouteDownstreamEncryption = suite.ConformanceTest{
ShortName: "HTTPRouteDownstreamEncryption",
Description: "A single Ingress in the higress-conformance-infra namespace for downstream encryption.",
Manifests: []string{"tests/httproute-downstream-encryption.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
// Prepare certificates and secrets for testcases
caCertOut, _, caCert, caKey := cert.MustGenerateCaCert(t)
svcCertOut, svcKeyOut := cert.MustGenerateCertWithCA(t, cert.ServerCertType, caCert, caKey, []string{"foo.com"})
cliCertOut, cliKeyOut := cert.MustGenerateCertWithCA(t, cert.ClientCertType, caCert, caKey, nil)
fooSecret := kubernetes.ConstructTLSSecret("higress-conformance-infra", "foo-secret", svcCertOut.Bytes(), svcKeyOut.Bytes())
fooSecretCACert := kubernetes.ConstructCASecret("higress-conformance-infra", "foo-secret-cacert", caCertOut.Bytes())
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{fooSecret, fooSecretCACert}, suite.Cleanup)
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: auth-tls-secret annotation",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo1",
Host: "foo1.com",
TLSConfig: &http.TLSConfig{
SNI: "foo1.com",
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo1",
Host: "foo1.com",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: ssl-cipher annotation, ingress of one cipher suite",
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo2",
Host: "foo2.com",
TLSConfig: &http.TLSConfig{
SNI: "foo2.com",
MaxVersion: tls.VersionTLS12,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA},
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo2",
Host: "foo2.com",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: ssl-cipher annotation, ingress of multiple cipher suites",
TargetBackend: "infra-backend-v3",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo3",
Host: "foo3.com",
TLSConfig: &http.TLSConfig{
SNI: "foo3.com",
MaxVersion: tls.VersionTLS12,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo3",
Host: "foo3.com",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: ssl-cipher annotation, TLSv1.2 cipher suites are invalid in TLSv1.3",
TargetBackend: "infra-backend-v3",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo3",
Host: "foo3.com",
TLSConfig: &http.TLSConfig{
SNI: "foo3.com",
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384},
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo3",
Host: "foo3.com",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
}
t.Run("Downstream encryption", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}

View File

@@ -0,0 +1,90 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/auth-tls-secret: foo-secret-cacert
name: httproute-downstream-encryption-auth
namespace: higress-conformance-infra
spec:
ingressClassName: higress
tls:
- hosts:
- "foo1.com"
secretName: foo-secret
rules:
- host: "foo1.com"
http:
paths:
- pathType: Exact
path: "/foo1"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/ssl-cipher: ECDHE-RSA-AES128-SHA
higress.io/auth-tls-secret: foo-secret-cacert
name: httproute-downstream-encryption-cipher-1
namespace: higress-conformance-infra
spec:
ingressClassName: higress
tls:
- hosts:
- "foo2.com"
secretName: foo-secret
rules:
- host: "foo2.com"
http:
paths:
- pathType: Exact
path: "/foo2"
backend:
service:
name: infra-backend-v2
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/ssl-cipher: ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES128-SHA,ECDHE-ECDSA-AES256-SHA
higress.io/auth-tls-secret: foo-secret-cacert
name: httproute-downstream-encryption-cipher-2
namespace: higress-conformance-infra
spec:
ingressClassName: higress
tls:
- hosts:
- "foo3.com"
secretName: foo-secret
rules:
- host: "foo3.com"
http:
paths:
- pathType: Exact
path: "/foo3"
backend:
service:
name: infra-backend-v3
port:
number: 8080

View File

@@ -57,7 +57,7 @@ var HttpForceRedirectHttps = suite.ConformanceTest{
},
}
t.Run("HTTPFORCEREDIRCTHTTPS", func(t *testing.T) {
t.Run("HttpForceRedirectHttps", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 (
"testing"
"github.com/alibaba/higress/test/ingress/conformance/utils/http"
"github.com/alibaba/higress/test/ingress/conformance/utils/suite"
)
func init() {
HigressConformanceTests = append(HigressConformanceTests, HTTPRouteFullPathRegex)
}
var HTTPRouteFullPathRegex = suite.ConformanceTest{
ShortName: "HTTPRouteFullPathRegex",
Description: "test for 'higress.io/full-path-regex' annotation",
Manifests: []string{"tests/httproute-full-path-regex.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testCases := []http.Assertion{
{
Request: http.AssertionRequest{
ActualRequest: http.Request{Path: "/foo/1234"},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
}, {
Request: http.AssertionRequest{
ActualRequest: http.Request{Path: "/bar/123"},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
}, {
Request: http.AssertionRequest{
ActualRequest: http.Request{Path: "/bar/1234"},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 404,
},
},
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
},
}
t.Run("Test for 'higress.io/full-path-regex'", func(t *testing.T) {
for _, testCase := range testCases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testCase)
}
})
},
}

View File

@@ -0,0 +1,53 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
name: httproute-ingress-use-regex
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- http:
paths:
- pathType: ImplementationSpecific
path: "/foo/[A-Z0-9]{3}"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/full-path-regex: "true"
name: httproute-higress-full-path-regex
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- http:
paths:
- pathType: ImplementationSpecific
path: "/bar/[A-Z0-9]{3}"
backend:
service:
name: infra-backend-v2
port:
number: 8080

View File

@@ -57,7 +57,7 @@ var HttpRedirectAsHttps = suite.ConformanceTest{
},
}
t.Run("HTTPREDIRCTASHTTPS", func(t *testing.T) {
t.Run("HttpRedirectAsHttps", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}

View File

@@ -32,6 +32,12 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: add one",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo1",
@@ -39,6 +45,8 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo1",
Host: "foo.com",
Headers: map[string]string{
"stage": "test",
},
@@ -53,6 +61,12 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: add more",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo2",
@@ -60,6 +74,8 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo2",
Host: "foo.com",
Headers: map[string]string{
"stage": "test",
"canary": "true",
@@ -75,6 +91,12 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: update one",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo3",
@@ -85,9 +107,10 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo3",
Host: "foo.com",
Headers: map[string]string{
"stage": "pro",
"canary": "true",
"stage": "pro",
},
},
},
@@ -100,19 +123,94 @@ var HTTPRouteRequestHeaderControl = suite.ConformanceTest{
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: update more",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo4",
Host: "foo.com",
Headers: map[string]string{
"stage": "test",
"canary": "true",
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo4",
Host: "foo.com",
Headers: map[string]string{
"stage": "pro",
"canary": "false",
},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 5: remove one",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo5",
Host: "foo.com",
Headers: map[string]string{
"stage": "test",
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo5",
Host: "foo.com",
},
AbsentHeaders: []string{"stage"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 6: remove more",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/foo6",
Host: "foo.com",
Headers: map[string]string{
"stage": "test",
"canary": "true",
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/foo6",
Host: "foo.com",
},
AbsentHeaders: []string{"stage", "canary"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,

View File

@@ -61,7 +61,7 @@ kind: Ingress
metadata:
annotations:
higress.io/request-header-control-update: stage pro
name: httproute-request-header-control-update
name: httproute-request-header-control-update-one
namespace: higress-conformance-infra
spec:
ingressClassName: higress
@@ -81,8 +81,10 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/request-header-control-remove: stage
name: httproute-request-header-control-remove
higress.io/request-header-control-update: |
stage pro
canary false
name: httproute-request-header-control-update-more
namespace: higress-conformance-infra
spec:
ingressClassName: higress
@@ -97,3 +99,45 @@ spec:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/request-header-control-remove: stage
name: httproute-request-header-control-remove-one
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Exact
path: "/foo5"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/request-header-control-remove: stage,canary
name: httproute-request-header-control-remove-more
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Exact
path: "/foo6"
backend:
service:
name: infra-backend-v1
port:
number: 8080

View File

@@ -30,9 +30,8 @@ var HTTPRouteRewritePath = suite.ConformanceTest{
Description: "A single Ingress in the higress-conformance-infra namespace uses the rewrite path.",
Manifests: []string{"tests/httproute-rewrite-path.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
t.Run("Rewrite HTTPRoute Path", func(t *testing.T) {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, http.Assertion{
testCases := []http.Assertion{
{
Request: http.AssertionRequest{
ActualRequest: http.Request{Path: "/svc/foo"},
ExpectedRequest: &http.ExpectedRequest{
@@ -50,7 +49,14 @@ var HTTPRouteRewritePath = suite.ConformanceTest{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
})
},
}
t.Run("Rewrite HTTPRoute Path", func(t *testing.T) {
for _, testCase := range testCases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testCase)
}
})
},
}

View File

@@ -31,3 +31,4 @@ spec:
name: infra-backend-v1
port:
number: 8080

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 (
"testing"
"github.com/alibaba/higress/test/ingress/conformance/utils/http"
"github.com/alibaba/higress/test/ingress/conformance/utils/suite"
)
func init() {
HigressConformanceTests = append(HigressConformanceTests, WasmPluginsRequestBlock)
}
var WasmPluginsRequestBlock = suite.ConformanceTest{
ShortName: "WasmPluginsRequestBlock",
Description: "The Ingress in the higress-conformance-infra namespace test the request-block wasmplugins.",
Manifests: []string{"tests/request-block.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/swagger.html",
UnfollowRedirect: true,
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
},
},
}
t.Run("WasmPlugins request-block", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}

View File

@@ -0,0 +1,45 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/app-root: "/foo"
name: httproute-app-root
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: request-block
namespace: higress-system
spec:
defaultConfig:
block_urls:
- "swagger.html"
url: file:///opt/plugins/wasm-go/extensions/request-block/plugin.wasm

View File

@@ -0,0 +1,145 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 cert
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type CertType int
const (
CACertType CertType = iota
ServerCertType
ClientCertType
)
const (
// RSABits defines the bit length of the RSA private key
RSABits = 2048
// ValidFor defines the certificate validity period
ValidFor = 365 * 24 * time.Hour
)
// MustGenerateCaCert must generate a CA certificate and private key.
// `certOut` and `keyOut` are PEM format buffers for certificate and private key, respectively.
// `caCert` and `caKey` are the corresponding structures.
func MustGenerateCaCert(t *testing.T) (certOut, keyOut *bytes.Buffer, caCert *x509.Certificate, caKey *rsa.PrivateKey) {
notBefore := time.Now()
notAfter := notBefore.Add(ValidFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1+int64(CACertType)), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
require.NoError(t, err, "failed to generate serial number")
caCert = &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "default",
Organization: []string{"Higress E2E Test"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
caKey, err = rsa.GenerateKey(rand.Reader, RSABits)
certOut, keyOut, err = GenerateCert(caCert, caKey, caCert, caKey)
return
}
// MustGenerateCertWithCA must generate a self-signed client/server certificate and private key
// using CA certificate and private key.
// `hosts` is used when CertType == ServerCertType
func MustGenerateCertWithCA(t *testing.T, certType CertType, caCert *x509.Certificate, caKey *rsa.PrivateKey, hosts []string) (certOut, keyOut *bytes.Buffer) {
notBefore := time.Now()
notAfter := notBefore.Add(ValidFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1+int64(certType)), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
require.NoError(t, err, "failed to generate serial number")
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "default",
Organization: []string{"Higress E2E Test"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
}
if certType == ServerCertType && hosts != nil {
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
}
privateKey, err := rsa.GenerateKey(rand.Reader, RSABits)
require.NoError(t, err, "failed to generate ras key")
certOut, keyOut, err = GenerateCert(template, privateKey, caCert, caKey)
return
}
// GenerateCert obtains the corresponding certificate and private key buffers
// using the certificate template and private key.
func GenerateCert(cert *x509.Certificate, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (
certOut, keyOut *bytes.Buffer, err error) {
var (
priv = key
pub = &priv.PublicKey
privPm = priv
)
if caKey != nil {
privPm = caKey
}
certDER, err := x509.CreateCertificate(rand.Reader, cert, caCert, pub, privPm)
if err != nil {
err = fmt.Errorf("failed to create certificate: %w", err)
return
}
certOut = new(bytes.Buffer)
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
if err != nil {
err = fmt.Errorf("failed creating cert: %w", err)
return
}
keyOut = new(bytes.Buffer)
privDER := x509.MarshalPKCS1PrivateKey(priv)
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDER})
if err != nil {
err = fmt.Errorf("failed creating key: %w", err)
return
}
return
}

View File

@@ -68,6 +68,10 @@ type TimeoutConfig struct {
// RequestTimeout represents the maximum time for making an HTTP Request with the roundtripper.
// Max value for conformant implementation: None
RequestTimeout time.Duration
// TLSHandshakeTimeout represents the maximum time for waiting for a TLS handshake. Zero means no timeout.
// Max value for conformant implementation: None
TLSHandshakeTimeout time.Duration
}
// DefaultTimeoutConfig populates a TimeoutConfig with the default values.
@@ -86,6 +90,7 @@ func DefaultTimeoutConfig() TimeoutConfig {
MaxTimeToConsistency: 30 * time.Second,
NamespacesMustBeReady: 300 * time.Second,
RequestTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
@@ -130,4 +135,7 @@ func SetupTimeoutConfig(timeoutConfig *TimeoutConfig) {
if timeoutConfig.RequestTimeout == 0 {
timeoutConfig.RequestTimeout = defaultTimeoutConfig.RequestTimeout
}
if timeoutConfig.TLSHandshakeTimeout == 0 {
timeoutConfig.TLSHandshakeTimeout = defaultTimeoutConfig.TLSHandshakeTimeout
}
}

View File

@@ -70,6 +70,38 @@ type Request struct {
Path string
Headers map[string]string
UnfollowRedirect bool
TLSConfig *TLSConfig
}
// TLSConfig defines the TLS configuration for the client.
// When this field is set, the HTTPS protocol is used.
type TLSConfig struct {
// MinVersion specifies the minimum TLS version,
// e.g. tls.VersionTLS12.
MinVersion uint16
// MinVersion specifies the maximum TLS version,
// e.g. tls.VersionTLS13.
MaxVersion uint16
// SNI is short for Server Name Indication.
// If this field is not specified, the value will be equal to `Host`.
SNI string
// CipherSuites can specify multiple client cipher suites,
// e.g. tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA.
CipherSuites []uint16
// Certificates defines the certificate chain
Certificates Certificates
}
// Certificates contains CA and client certificate chain
type Certificates struct {
CACerts [][]byte
ClientKeyPairs []ClientKeyPair
}
// ClientKeyPair is a pair of client certificate and private key.
type ClientKeyPair struct {
ClientCert []byte
ClientKey []byte
}
// ExpectedRequest defines expected properties of a request that reaches a backend.
@@ -102,6 +134,36 @@ const requiredConsecutiveSuccesses = 3
func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripper.RoundTripper, timeoutConfig config.TimeoutConfig, gwAddr string, expected Assertion) {
t.Helper()
var (
scheme = "http"
protocol = "HTTP"
tlsConfig *roundtripper.TLSConfig
)
if expected.Request.ActualRequest.TLSConfig != nil {
scheme = "https"
protocol = "HTTPS"
clientKeyPairs := make([]roundtripper.ClientKeyPair, 0, len(expected.Request.ActualRequest.TLSConfig.Certificates.ClientKeyPairs))
for _, keyPair := range expected.Request.ActualRequest.TLSConfig.Certificates.ClientKeyPairs {
clientKeyPairs = append(clientKeyPairs, roundtripper.ClientKeyPair{
ClientCert: keyPair.ClientCert,
ClientKey: keyPair.ClientKey,
})
}
tlsConfig = &roundtripper.TLSConfig{
MinVersion: expected.Request.ActualRequest.TLSConfig.MinVersion,
MaxVersion: expected.Request.ActualRequest.TLSConfig.MaxVersion,
SNI: expected.Request.ActualRequest.TLSConfig.SNI,
CipherSuites: expected.Request.ActualRequest.TLSConfig.CipherSuites,
Certificates: roundtripper.Certificates{
CACert: expected.Request.ActualRequest.TLSConfig.Certificates.CACerts,
ClientKeyPairs: clientKeyPairs,
},
}
if tlsConfig.SNI == "" {
tlsConfig.SNI = expected.Request.ActualRequest.Host
}
}
if expected.Request.ActualRequest.Method == "" {
expected.Request.ActualRequest.Method = "GET"
}
@@ -110,17 +172,18 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
expected.Response.ExpectedResponse.StatusCode = 200
}
t.Logf("Making %s request to http://%s%s", expected.Request.ActualRequest.Method, gwAddr, expected.Request.ActualRequest.Path)
t.Logf("Making %s request to %s://%s%s", expected.Request.ActualRequest.Method, scheme, gwAddr, expected.Request.ActualRequest.Path)
path, query, _ := strings.Cut(expected.Request.ActualRequest.Path, "?")
req := roundtripper.Request{
Method: expected.Request.ActualRequest.Method,
Host: expected.Request.ActualRequest.Host,
URL: url.URL{Scheme: "http", Host: gwAddr, Path: path, RawQuery: query},
Protocol: "HTTP",
URL: url.URL{Scheme: scheme, Host: gwAddr, Path: path, RawQuery: query},
Protocol: protocol,
Headers: map[string][]string{},
UnfollowRedirect: expected.Request.ActualRequest.UnfollowRedirect,
TLSConfig: tlsConfig,
}
if expected.Request.ActualRequest.Headers != nil {

View File

@@ -20,7 +20,6 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
@@ -35,11 +34,8 @@ import (
// ensure auth plugins are loaded
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
const (
rsaBits = 2048
validFor = 365 * 24 * time.Hour
"github.com/alibaba/higress/test/ingress/conformance/utils/cert"
)
// MustCreateSelfSignedCertSecret creates a self-signed SSL certificate and stores it in a secret
@@ -47,49 +43,56 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string,
require.Greater(t, len(hosts), 0, "require a non-empty hosts for Subject Alternate Name values")
var serverKey, serverCert bytes.Buffer
host := strings.Join(hosts, ",")
require.NoError(t, generateRSACert(host, &serverKey, &serverCert), "failed to generate RSA certificate")
data := map[string][]byte{
corev1.TLSCertKey: serverCert.Bytes(),
corev1.TLSPrivateKeyKey: serverKey.Bytes(),
}
return ConstructTLSSecret(namespace, secretName, serverCert.Bytes(), serverKey.Bytes())
}
newSecret := &corev1.Secret{
// ConstructTLSSecret constructs a secret of type "kubernetes.io/tls"
func ConstructTLSSecret(namespace, secretName string, cert, key []byte) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Type: corev1.SecretTypeTLS,
Data: data,
Data: map[string][]byte{
corev1.TLSCertKey: cert,
corev1.TLSPrivateKeyKey: key,
},
}
return newSecret
}
// generateRSACert generates a basic self signed certificate valir for a year
func generateRSACert(host string, keyOut, certOut io.Writer) error {
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
// ConstructCASecret construct a CA secret of type "Opaque"
func ConstructCASecret(namespace, secretName string, cert []byte) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
corev1.ServiceAccountRootCAKey: cert,
},
}
notBefore := time.Now()
notAfter := notBefore.Add(validFor)
}
// generateRSACert generates a basic self signed certificate valid for a year
func generateRSACert(host string, keyOut, certOut io.Writer) error {
notBefore := time.Now()
notAfter := notBefore.Add(cert.ValidFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %w", err)
}
template := x509.Certificate{
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "default",
Organization: []string{"Acme Co"},
Organization: []string{"Higress E2E Test"},
},
NotBefore: notBefore,
NotAfter: notAfter,
@@ -108,18 +111,13 @@ func generateRSACert(host string, keyOut, certOut io.Writer) error {
}
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
priv, err := rsa.GenerateKey(rand.Reader, cert.RSABits)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
return fmt.Errorf("failed to generate key: %w", err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return fmt.Errorf("failed creating cert: %w", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
return fmt.Errorf("failed creating key: %w", err)
certOut, keyOut, err = cert.GenerateCert(template, priv, template, nil)
if err != nil {
return fmt.Errorf("failed to generate rsa certificate: %w", err)
}
return nil

View File

@@ -16,6 +16,8 @@ package roundtripper
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
@@ -41,6 +43,29 @@ type Request struct {
Method string
Headers map[string][]string
UnfollowRedirect bool
TLSConfig *TLSConfig
}
// TLSConfig defines the TLS configuration for the client.
// When this field is set, the HTTPS protocol is used.
type TLSConfig struct {
MinVersion uint16
MaxVersion uint16
SNI string
CipherSuites []uint16
Certificates Certificates
}
// Certificates defines the self-signed client and CA certificate chain
type Certificates struct {
CACert [][]byte
ClientKeyPairs []ClientKeyPair
}
// ClientKeyPair is a pair of client certificate and private key.
type ClientKeyPair struct {
ClientCert []byte
ClientKey []byte
}
// CapturedRequest contains request metadata captured from an echoserver
@@ -95,6 +120,35 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
}
}
if request.TLSConfig != nil {
pool := x509.NewCertPool()
for _, caCert := range request.TLSConfig.Certificates.CACert {
pool.AppendCertsFromPEM(caCert)
}
var clientCerts []tls.Certificate
for _, keyPair := range request.TLSConfig.Certificates.ClientKeyPairs {
newClientCert, err := tls.X509KeyPair(keyPair.ClientCert, keyPair.ClientKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to load client key pair: %w", err)
}
clientCerts = append(clientCerts, newClientCert)
}
client.Transport = &http.Transport{
TLSHandshakeTimeout: d.TimeoutConfig.TLSHandshakeTimeout,
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
MinVersion: request.TLSConfig.MinVersion,
MaxVersion: request.TLSConfig.MaxVersion,
ServerName: request.TLSConfig.SNI,
CipherSuites: request.TLSConfig.CipherSuites,
RootCAs: pool,
Certificates: clientCerts,
InsecureSkipVerify: true,
},
}
}
method := "GET"
if request.Method != "" {
method = request.Method
@@ -130,6 +184,7 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
if err != nil {
return nil, nil, err
}
defer client.CloseIdleConnections()
defer resp.Body.Close()
if d.Debug {

View File

@@ -28,6 +28,8 @@ import (
"github.com/alibaba/higress/test/ingress/conformance/utils/suite"
)
var isWasmPluginTest = flag.Bool("isWasmPluginTest", false, "")
func TestHigressConformanceTests(t *testing.T) {
flag.Parse()
@@ -49,29 +51,39 @@ func TestHigressConformanceTests(t *testing.T) {
})
cSuite.Setup(t)
var higressTests []suite.ConformanceTest
higressTests := []suite.ConformanceTest{
tests.HTTPRouteSimpleSameNamespace,
tests.HTTPRouteHostNameSameNamespace,
tests.HTTPRouteRewritePath,
tests.HTTPRouteRewriteHost,
tests.HTTPRouteCanaryHeader,
tests.HTTPRouteEnableCors,
tests.HTTPRouteEnableIgnoreCase,
tests.HTTPRouteMatchMethods,
tests.HTTPRouteMatchQueryParams,
tests.HTTPRouteMatchHeaders,
tests.HTTPRouteAppRoot,
tests.HTTPRoutePermanentRedirect,
tests.HTTPRoutePermanentRedirectCode,
tests.HTTPRouteTemporalRedirect,
tests.HTTPRouteSameHostAndPath,
tests.HTTPRouteCanaryHeaderWithCustomizedHeader,
tests.HTTPRouteWhitelistSourceRange,
tests.HTTPRouteCanaryWeight,
tests.HTTPRouteMatchPath,
tests.HttpForceRedirectHttps,
tests.HttpRedirectAsHttps,
if *isWasmPluginTest {
higressTests = []suite.ConformanceTest{
tests.WasmPluginsRequestBlock,
}
} else {
higressTests = []suite.ConformanceTest{
tests.HTTPRouteSimpleSameNamespace,
tests.HTTPRouteHostNameSameNamespace,
tests.HTTPRouteRewritePath,
tests.HTTPRouteRewriteHost,
tests.HTTPRouteCanaryHeader,
tests.HTTPRouteEnableCors,
tests.HTTPRouteEnableIgnoreCase,
tests.HTTPRouteMatchMethods,
tests.HTTPRouteMatchQueryParams,
tests.HTTPRouteMatchHeaders,
tests.HTTPRouteAppRoot,
tests.HTTPRoutePermanentRedirect,
tests.HTTPRoutePermanentRedirectCode,
tests.HTTPRouteTemporalRedirect,
tests.HTTPRouteSameHostAndPath,
tests.HTTPRouteCanaryHeaderWithCustomizedHeader,
tests.HTTPRouteWhitelistSourceRange,
tests.HTTPRouteCanaryWeight,
tests.HTTPRouteMatchPath,
tests.HttpForceRedirectHttps,
tests.HttpRedirectAsHttps,
tests.HTTPRouteRequestHeaderControl,
tests.HTTPRouteDownstreamEncryption,
tests.HTTPRouteFullPathRegex,
}
}
cSuite.Run(t, higressTests)

View File

@@ -0,0 +1,36 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
# 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.
#!/usr/bin/env bash
set -euo pipefail
cd ./plugins/wasm-go/
INNER_PLUGIN_NAME=${PLUGIN_NAME-""}
if [ ! -n "$INNER_PLUGIN_NAME" ]; then
EXTENSIONS_DIR=$(pwd)"/extensions/"
echo "build all wasmplugins under folder of $EXTENSIONS_DIR"
for file in `ls $EXTENSIONS_DIR`
do
if [ -d $EXTENSIONS_DIR$file ]; then
name=${file##*/}
echo "build wasmplugin name of $name"
PLUGIN_NAME=${name} make build
fi
done
else
echo "build wasmplugin name of $INNER_PLUGIN_NAME"
PLUGIN_NAME=${INNER_PLUGIN_NAME} make build
fi

View File

@@ -1,32 +0,0 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
# 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.
# cluster.conf
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP

View File

@@ -20,6 +20,48 @@ set -euo pipefail
CLUSTER_NAME=${CLUSTER_NAME:-"higress"}
METALLB_VERSION=${METALLB_VERSION:-"v0.13.7"}
KIND_NODE_TAG=${KIND_NODE_TAG:-"v1.25.3"}
PROJECT_DIR=$(pwd)
echo ${KIND_NODE_TAG}
echo ${CLUSTER_NAME}
cat <<EOF > "tools/hack/cluster.conf"
# Copyright (c) 2022 Alibaba Group Holding Ltd.
# 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.
# cluster.conf
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
extraMounts:
- hostPath: ${PROJECT_DIR}/plugins
containerPath: /opt/plugins
EOF
## Create kind cluster.
if [[ -z "${KIND_NODE_TAG}" ]]; then