mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 13:10:50 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d7124454 | ||
|
|
ec6a185adc | ||
|
|
f9ffda288b | ||
|
|
2dbe41324a | ||
|
|
80d6ecfddb | ||
|
|
b23fae7a12 | ||
|
|
2c19d97252 | ||
|
|
efd7ccd5fe | ||
|
|
81fd0d6386 | ||
|
|
176ddc6963 | ||
|
|
44637c2449 |
116
.github/workflows/build-and-test.yaml
vendored
116
.github/workflows/build-and-test.yaml
vendored
@@ -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
3
.gitignore
vendored
@@ -12,4 +12,5 @@ bazel-out
|
||||
bazel-testlogs
|
||||
bazel-wasm-cpp
|
||||
tools/bin/
|
||||
helm/**/charts/**.tgz
|
||||
helm/**/charts/**.tgz
|
||||
tools/hack/cluster.conf
|
||||
@@ -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
|
||||
|
||||
1
helm/core/.helmignore
Normal file
1
helm/core/.helmignore
Normal file
@@ -0,0 +1 @@
|
||||
crds/customresourcedefinitions.gen_lt1.16.yaml
|
||||
176
helm/core/crds/customresourcedefinitions.gen_lt1.16.yaml
Normal file
176
helm/core/crds/customresourcedefinitions.gen_lt1.16.yaml
Normal 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: {}
|
||||
---
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
230
plugins/wasm-go/extensions/cors/README.md
Normal file
230
plugins/wasm-go/extensions/cors/README.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 功能说明
|
||||
|
||||
`cors` 插件可以为服务端启用 CORS(Cross-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,比如:GET,POST 等。可以使用 * 来表示允许所有 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
|
||||
1
plugins/wasm-go/extensions/cors/VERSION
Normal file
1
plugins/wasm-go/extensions/cors/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
457
plugins/wasm-go/extensions/cors/config/cors_config.go
Normal file
457
plugins/wasm-go/extensions/cors/config/cors_config.go
Normal 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
|
||||
}
|
||||
408
plugins/wasm-go/extensions/cors/config/cors_config_test.go
Normal file
408
plugins/wasm-go/extensions/cors/config/cors_config_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
67
plugins/wasm-go/extensions/cors/cors.yaml
Normal file
67
plugins/wasm-go/extensions/cors/cors.yaml
Normal 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
|
||||
78
plugins/wasm-go/extensions/cors/envoy.yaml
Normal file
78
plugins/wasm-go/extensions/cors/envoy.yaml
Normal 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
|
||||
21
plugins/wasm-go/extensions/cors/go.mod
Normal file
21
plugins/wasm-go/extensions/cors/go.mod
Normal 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
|
||||
)
|
||||
20
plugins/wasm-go/extensions/cors/go.sum
Normal file
20
plugins/wasm-go/extensions/cors/go.sum
Normal 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=
|
||||
169
plugins/wasm-go/extensions/cors/main.go
Normal file
169
plugins/wasm-go/extensions/cors/main.go
Normal 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
|
||||
}
|
||||
@@ -15,7 +15,13 @@ Higress e2e tests are mainly focusing on two parts for now:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
89
test/ingress/conformance/tests/httproute-full-path-regex.go
Normal file
89
test/ingress/conformance/tests/httproute-full-path-regex.go
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,3 +31,4 @@ spec:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
|
||||
|
||||
59
test/ingress/conformance/tests/request-block.go
Normal file
59
test/ingress/conformance/tests/request-block.go
Normal 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)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
45
test/ingress/conformance/tests/request-block.yaml
Normal file
45
test/ingress/conformance/tests/request-block.yaml
Normal 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
|
||||
145
test/ingress/conformance/utils/cert/cert.go
Normal file
145
test/ingress/conformance/utils/cert/cert.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
36
tools/hack/build-wasm-plugins.sh
Executable file
36
tools/hack/build-wasm-plugins.sh
Executable 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user