Compare commits

..

4 Commits

Author SHA1 Message Date
澄潭
5140372159 rel: Release verison 1.3.4 (#828) 2024-02-20 11:56:16 +08:00
罗泽轩
f277d4f6ae test: allow specifying HTTP protocol (#822)
Signed-off-by: spacewander <spacewanderlzx@gmail.com>
2024-02-20 11:29:05 +08:00
澄潭
ae20420179 Increase health check timeout of the first readiness probe from pilot-agent (#827) 2024-02-20 11:06:42 +08:00
Se7en
a138a037ad feat: implement custom-response plugin in the golang version (#689) 2024-02-20 09:39:41 +08:00
17 changed files with 895 additions and 13 deletions

View File

@@ -1 +1 @@
v1.3.4-rc.2
v1.3.4

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.3.4-rc.2
appVersion: 1.3.4
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -10,4 +10,4 @@ name: higress-core
sources:
- http://github.com/alibaba/higress
type: application
version: 1.3.4-rc.2
version: 1.3.4

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 1.3.4-rc.2
version: 1.3.4
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.3.2
digest: sha256:aa10fa4ae01c7e7f8297db5fe6f4752aa6526e2411d46c93abddbb4e7e84d36d
generated: "2024-02-01T19:33:51.268079+08:00"
digest: sha256:1a5849d5ead79151e42ce419a96669f6691567e6fbf155643a928e6b62c3daa1
generated: "2024-02-20T11:54:00.898489+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.3.4-rc.2
appVersion: 1.3.4
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 1.3.4-rc.2
version: 1.3.4
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.3.2
type: application
version: 1.3.4-rc.2
version: 1.3.4

View File

@@ -0,0 +1,84 @@
<p>
<a href="README_EN.md"> English </a> | 中文
</p>
# 功能说明
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| -------- | -------- | -------- | -------- | -------- |
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
| headers | array of string | 选填 | - | 自定义 HTTP 应答头key 和 value 用`=`分隔 |
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
# 配置示例
## Mock 应答场景
```yaml
status_code: 200
headers:
- Content-Type=application/json
- Hello=World
body: "{\"hello\":\"world\"}"
```
根据该配置,请求将返回自定义应答如下:
```text
HTTP/1.1 200 OK
Content-Type: application/json
Hello: World
Content-Length: 17
{"hello":"world"}
```
## 触发限流时自定义响应
```yaml
enable_on_status:
- 429
status_code: 302
headers:
- Location=https://example.com
```
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
```text
HTTP/1.1 302 Found
Location: https://example.com
```
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
## 对特定路由或域名开启
```yaml
# 使用 matchRules 字段进行细粒度规则配置
matchRules:
# 规则一:按 Ingress 名称匹配生效
- ingress:
- default/foo
- default/bar
body: "{\"hello\":\"world\"}"
# 规则二:按域名匹配生效
- domain:
- "*.example.com"
- test.com
enable_on_status:
- 429
status_code: 200
headers:
- Content-Type=application/json
body: "{\"errmsg\": \"rate limited\"}"
```
此例 `ingress` 中指定的 `default/foo``default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress当匹配到这两个 Ingress 时,将使用此段配置;
此例 `domain` 中指定的 `*.example.com``test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。

View File

@@ -0,0 +1,84 @@
<p>
English | <a href="README.md">中文</a>
</p>
# Description
`custom-response` plugin implements a function of sending custom responses, including custom HTTP response status codes, HTTP response headers and HTTP response body, which can be used in the scenarios of response mocking and sending a custom response for specific status codes, such as customizing the response for rate-limited requests.
# Configuration Fields
| Name | Type | Requirement | Default Value | Description |
| -------- | -------- | -------- | -------- | -------- |
| status_code | number | Optional | 200 | Custom HTTP response status code |
| headers | array of string | Optional | - | Custom HTTP response header. Key and value shall be separated using `=`. |
| body | string | Optional | - | Custom HTTP response body |
| enable_on_status | array of number | Optional | - | The original response status code to match. Generate the custom response only the actual status code matches the configuration. Ignore the status code match if left unconfigured. |
# Configuration Samples
## Mock Responses
```yaml
status_code: 200
headers:
- Content-Type=application/json
- Hello=World
body: "{\"hello\":\"world\"}"
```
According to the configuration above, all the requests will get the following custom response:
```text
HTTP/1.1 200 OK
Content-Type: application/json
Hello: World
Content-Length: 17
{"hello":"world"}
```
## Send a Custom Response when Rate-Limited
```yaml
enable_on_status:
- 429
status_code: 302
headers:
- Location=https://example.com
```
When rate-limited, normally gateway will return a status code of `429` . Now, rate-limited requests will get the following custom response:
```text
HTTP/1.1 302 Found
Location: https://example.com
```
So based on the 302 redirecting mechanism provided by browsers, this can redirect rate-limited users to other pages, for example, a static page hosted on CDN.
If you'd like to send other responses when rate-limited, please add other fields into the configuration, referring to the Mock Responses scenario.
## Only Enabled for Specific Routes or Domains
```yaml
# Use matchRules field for fine-grained rule configurations
matchRules:
# Rule 1: Match by Ingress name
- ingress:
- default/foo
- default/bar
body: "{\"hello\":\"world\"}"
# Rule 2: Match by domain
- domain:
- "*.example.com"
- test.com
enable_on_status:
- 429
status_code: 200
headers:
- Content-Type=application/json
body: "{\"errmsg\": \"rate limited\"}"
```
In the rule sample of `ingress`, `default/foo` and `default/bar` are the Ingresses named foo and bar in the default namespace. When the current Ingress names matches the configuration, the rule following shall be applied.
In the rule sample of `domain`, `*.example.com` and `test.com` are the domain names used for request matching. When the current domain name matches the configuration, the rule following shall be applied.
All rules shall be checked following the order of items in the `matchRules` field, The first matched rule will be applied. All remained will be ignored.

View File

@@ -0,0 +1,71 @@
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmdemo
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmdemo
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/main.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"headers": ["key1=value1", "key2=value2"],
"status_code": 200,
"enable_on_status": [200, 201],
"body": "{\"hello\":\"world\"}"
}
- name: envoy.filters.http.router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80

View File

@@ -0,0 +1,20 @@
module github.com/alibaba/higress/plugins/wasm-go/extensions/basic-auth
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0
github.com/pkg/errors v0.9.1
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
github.com/tidwall/gjson v1.14.3
)
require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)

View File

@@ -0,0 +1,15 @@
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/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/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=

View File

@@ -0,0 +1,135 @@
// 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 (
"encoding/json"
"fmt"
"strconv"
"strings"
"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(
"custom-response",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
)
}
type CustomResponseConfig struct {
statusCode uint32
headers [][2]string
body string
enableOnStatus []uint32
contentType string
}
func parseConfig(gjson gjson.Result, config *CustomResponseConfig, log wrapper.Log) error {
headersArray := gjson.Get("headers").Array()
config.headers = make([][2]string, 0, len(headersArray))
for _, v := range headersArray {
kv := strings.SplitN(v.String(), "=", 2)
if len(kv) == 2 {
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if strings.EqualFold(key, "content-type") {
config.contentType = value
} else if strings.EqualFold(key, "content-length") {
continue
} else {
config.headers = append(config.headers, [2]string{key, value})
}
} else {
return fmt.Errorf("invalid header pair format: %s", v.String())
}
}
config.body = gjson.Get("body").String()
if config.contentType == "" && config.body != "" {
if json.Valid([]byte(config.body)) {
config.contentType = "application/json; charset=utf-8"
} else {
config.contentType = "text/plain; charset=utf-8"
}
}
config.headers = append(config.headers, [2]string{"content-type", config.contentType})
config.statusCode = 200
if gjson.Get("status_code").Exists() {
statusCode := gjson.Get("status_code")
parsedStatusCode, err := strconv.Atoi(statusCode.String())
if err != nil {
return fmt.Errorf("invalid status code value: %s", statusCode.String())
}
config.statusCode = uint32(parsedStatusCode)
}
enableOnStatusArray := gjson.Get("enable_on_status").Array()
config.enableOnStatus = make([]uint32, 0, len(enableOnStatusArray))
for _, v := range enableOnStatusArray {
parsedEnableOnStatus, err := strconv.Atoi(v.String())
if err != nil {
return fmt.Errorf("invalid enable_on_status value: %s", v.String())
}
config.enableOnStatus = append(config.enableOnStatus, uint32(parsedEnableOnStatus))
}
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
if len(config.enableOnStatus) != 0 {
return types.ActionContinue
}
err := proxywasm.SendHttpResponse(config.statusCode, config.headers, []byte(config.body), -1)
if err != nil {
log.Errorf("send http response failed: %v", err)
}
return types.ActionPause
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
// enableOnStatus is not empty, compare the status code.
// if match the status code, mock the response.
statusCodeStr, err := proxywasm.GetHttpResponseHeader(":status")
if err != nil {
log.Errorf("get http response status code failed: %v", err)
return types.ActionContinue
}
statusCode, err := strconv.ParseUint(statusCodeStr, 10, 32)
if err != nil {
log.Errorf("parse http response status code failed: %v", err)
return types.ActionContinue
}
for _, v := range config.enableOnStatus {
if uint32(statusCode) == v {
err = proxywasm.SendHttpResponse(config.statusCode, config.headers, []byte(config.body), -1)
if err != nil {
log.Errorf("send http response failed: %v", err)
}
}
}
return types.ActionContinue
}

View File

@@ -0,0 +1,159 @@
// 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/e2e/conformance/utils/http"
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
)
func init() {
Register(WasmPluginsCustomResponse)
}
var WasmPluginsCustomResponse = suite.ConformanceTest{
ShortName: "WasmPluginsCustomResponse",
Description: "The Ingress in the higress-conformance-infra namespace test the custom-response WASM plugin.",
Manifests: []string{"tests/go-wasm-custom-response.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: Match global config",
CompareTarget: http.CompareTargetResponse,
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
Headers: map[string]string{
"key1": "value1",
},
ContentType: http.ContentTypeApplicationJson,
Body: []byte("{\"hello\":\"foo\"}"),
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: Match rule config",
CompareTarget: http.CompareTargetResponse,
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "bar.com",
Path: "/",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
Headers: map[string]string{
"key2": "value2",
},
ContentType: http.ContentTypeApplicationJson,
Body: []byte("{\"hello\":\"bar\"}"),
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: Match enable_on_status",
CompareTarget: http.CompareTargetResponse,
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "baz.com",
Path: "/",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
Headers: map[string]string{
"key3": "value3",
},
ContentType: http.ContentTypeApplicationJson,
Body: []byte("{\"hello\":\"baz\"}"),
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: Not match enable_on_status",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
CompareTarget: http.CompareTargetRequest,
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "baz.com",
Path: "/",
Headers: map[string]string{"Authorization": "Basic YWRtaW46MTIzNDU2"}, // base64("admin:123456")
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Host: "baz.com",
Path: "/",
Headers: map[string]string{
"X-Mse-Consumer": "consumer1",
},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 5: Change status code",
CompareTarget: http.CompareTargetResponse,
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "qux.com",
Path: "/",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 201,
Headers: map[string]string{
"key5": "value5",
},
ContentType: http.ContentTypeApplicationJson,
Body: []byte("{\"hello\":\"qux\"}"),
},
},
},
}
t.Run("WasmPlugins custom-response", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}

View File

@@ -0,0 +1,118 @@
# 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:
name: wasmplugin-custom-response
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
- host: "bar.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
- host: "baz.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
- host: "qux.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: custom-response
namespace: higress-system
spec:
priority: 200 # ensure custom-response plugin is applied before basic-auth plugin
defaultConfig:
headers:
- key1=value1
"body": "{\"hello\":\"foo\"}"
matchRules:
- domain:
- bar.com
config:
headers:
- key2=value2
"body": "{\"hello\":\"bar\"}"
- domain:
- baz.com
config:
enable_on_status:
- 401
headers:
- key3=value3
"body": "{\"hello\":\"baz\"}"
- domain:
- qux.com
config:
status_code: 201
headers:
- key5=value5
"body": "{\"hello\":\"qux\"}"
url: file:///opt/plugins/wasm-go/extensions/custom-response/plugin.wasm
---
# test enable_on_status 401
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: basic-auth
namespace: higress-system
spec:
priority: 100
defaultConfig:
consumers:
- credential: admin:123456
name: consumer1
global_auth: false
matchRules:
- config:
allow:
- consumer1
domain:
- baz.com
url: file:///opt/plugins/wasm-go/extensions/basic-auth/plugin.wasm

View File

@@ -89,6 +89,70 @@ var WasmPluginsSniMisdirect = suite.ConformanceTest{
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: https/2.0 request with sni and same with host",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Protocol: "HTTP/2.0",
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{
"Content-Type": "text/plain",
},
TLSConfig: &http.TLSConfig{
SNI: "foo.com",
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: https/2.0 request with sni and not same with host",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Protocol: "HTTP/2.0",
Host: "bar.com",
Path: "/foo",
Headers: map[string]string{
"Content-Type": "text/plain",
},
TLSConfig: &http.TLSConfig{
SNI: "foo.com",
Certificates: http.Certificates{
CACerts: [][]byte{caCertOut.Bytes()},
ClientKeyPairs: []http.ClientKeyPair{{
ClientCert: cliCertOut.Bytes(),
ClientKey: cliKeyOut.Bytes()},
},
},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 421,
},
},
},
}
t.Run("WasmPlugin sni-misdirect", func(t *testing.T) {

View File

@@ -23,10 +23,10 @@ spec:
ingressClassName: higress
tls:
- hosts:
- "foo.com"
- "*.com"
secretName: foo-secret
rules:
- host: "foo.com"
- host: "*.com"
http:
paths:
- pathType: Exact

View File

@@ -88,6 +88,7 @@ const (
// values can be provided, as a comma-separated value.
type Request struct {
Host string
Protocol string
Method string
Path string
Headers map[string]string
@@ -162,12 +163,10 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
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{
@@ -213,6 +212,11 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
path, query, _ := strings.Cut(expected.Request.ActualRequest.Path, "?")
protocol := "HTTP/1.1"
if expected.Request.ActualRequest.Protocol != "" {
protocol = expected.Request.ActualRequest.Protocol
}
req := roundtripper.Request{
Method: expected.Request.ActualRequest.Method,
Host: expected.Request.ActualRequest.Host,

View File

@@ -26,6 +26,8 @@ import (
"net/url"
"regexp"
"golang.org/x/net/http2"
"github.com/alibaba/higress/test/e2e/conformance/utils/config"
)
@@ -109,6 +111,53 @@ type DefaultRoundTripper struct {
TimeoutConfig config.TimeoutConfig
}
func (d *DefaultRoundTripper) initTransport(client *http.Client, protocol string, tlsConfig *TLSConfig) error {
var tlsClientConfig *tls.Config
if tlsConfig != nil {
pool := x509.NewCertPool()
for _, caCert := range tlsConfig.Certificates.CACert {
pool.AppendCertsFromPEM(caCert)
}
var clientCerts []tls.Certificate
for _, keyPair := range tlsConfig.Certificates.ClientKeyPairs {
newClientCert, err := tls.X509KeyPair(keyPair.ClientCert, keyPair.ClientKey)
if err != nil {
return fmt.Errorf("failed to load client key pair: %w", err)
}
clientCerts = append(clientCerts, newClientCert)
}
tlsClientConfig = &tls.Config{
MinVersion: tlsConfig.MinVersion,
MaxVersion: tlsConfig.MaxVersion,
ServerName: tlsConfig.SNI,
CipherSuites: tlsConfig.CipherSuites,
RootCAs: pool,
Certificates: clientCerts,
InsecureSkipVerify: true,
}
}
switch protocol {
case "HTTP/2.0":
tr := &http2.Transport{}
if tlsClientConfig != nil {
tr.TLSClientConfig = tlsClientConfig
}
client.Transport = tr
default: // HTTP1
if tlsClientConfig != nil {
client.Transport = &http.Transport{
TLSHandshakeTimeout: d.TimeoutConfig.TLSHandshakeTimeout,
DisableKeepAlives: true,
TLSClientConfig: tlsClientConfig,
}
}
}
return nil
}
// CaptureRoundTrip makes a request with the provided parameters and returns the
// captured request and response from echoserver. An error will be returned if
// there is an error running the function but not if an HTTP error status code
@@ -152,6 +201,8 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
}
}
d.initTransport(client, request.Protocol, request.TLSConfig)
method := "GET"
if request.Method != "" {
method = request.Method

View File

@@ -0,0 +1,77 @@
// 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 roundtripper
import (
"crypto/tls"
"crypto/x509"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/net/http2"
)
func TestTransport(t *testing.T) {
req := Request{
Protocol: "HTTP/2.0",
}
tests := []struct {
name string
req Request
prevTransport http.RoundTripper
tlsConfig *TLSConfig
transport http.RoundTripper
}{
{
name: "default",
req: Request{},
},
{
name: "http2",
req: req,
transport: &http2.Transport{},
},
{
name: "http1",
req: Request{
Protocol: "HTTP/1.1",
},
},
{
name: "https",
req: req,
tlsConfig: &TLSConfig{
SNI: "www.example.com",
},
transport: &http2.Transport{
TLSClientConfig: &tls.Config{
RootCAs: x509.NewCertPool(),
ServerName: "www.example.com",
InsecureSkipVerify: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := DefaultRoundTripper{}
c := http.Client{}
d.initTransport(&c, tt.req.Protocol, tt.tlsConfig)
assert.Equal(t, tt.transport, c.Transport)
})
}
}