mirror of
https://github.com/alibaba/higress.git
synced 2026-03-09 11:10:49 +08:00
feat: update custom-response plugin to returns different contents for different response status (#2002)
This commit is contained in:
@@ -14,34 +14,152 @@ description: 自定义应答插件配置参考
|
||||
插件执行优先级:`910`
|
||||
|
||||
## 配置字段
|
||||
### 新版本-支持多种返回
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------|------------------|------|-----|-------------------------------------|
|
||||
| rules | array of object | 必填 | - | 规则组 |
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
`rules`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|---------------------------|------|-----|------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| `headers` | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| `body` | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| `enable_on_status` | array of string or number | 选填 | - | 匹配原始状态码,生成自定义响应。可填写精确值如:`200`,`404`等,也可以模糊匹配例如:`2xx`来匹配200-299之间的状态码,`20x`来匹配200-209之间的状态码,x代表任意一位数字。不填写时,不判断原始状态码,取第一个`enable_on_status`为空的规则作为默认规则 |
|
||||
|
||||
#### 模糊匹配规则:
|
||||
* 长度为3
|
||||
* 至少一位数字
|
||||
* 至少一位x(不区分大小写)
|
||||
|
||||
| 规则 | 匹配内容 |
|
||||
|-----|------------------------------------------------------------------------------------------|
|
||||
| 40x | 400-409;前两位为40的情况 |
|
||||
| 1x4 | 104,114,124,134,144,154,164,174,184,194;第一位和第三位分别为1和4的情况 |
|
||||
| x23 | 023,123,223,323,423,523,623,723,823,923;第二位和第三位为23的情况 |
|
||||
| 4xx | 400-499;第一位为4的情况 |
|
||||
| x4x | 040-049,140-149,240-249,340-349,440-449,540-549,640-649,740-749,840-849,940-949;第二位为4的情况 |
|
||||
| xx4 | 尾数为4的情况 |
|
||||
|
||||
### 老版本-只支持一种返回
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- |------| -------- |---------------------------------|
|
||||
| `status_code` | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| `headers` | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| `body` | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| `enable_on_status` | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
|
||||
匹配优先级:精确匹配 > 模糊匹配 > 默认配置(第一个enable_on_status为空的配置)
|
||||
|
||||
## 配置示例
|
||||
|
||||
### Mock 应答场景
|
||||
### 新版本-不同状态码不同应答场景
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
- 201
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 404"}'
|
||||
enable_on_status:
|
||||
- 404
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
根据该配置,请求将返回自定义应答如下:
|
||||
根据该配置,200、201请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
根据该配置,404请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 400"}
|
||||
```
|
||||
|
||||
### 新版本-模糊匹配场景
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 40x"}'
|
||||
enable_on_status:
|
||||
- '40x'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
根据该配置,200状态码将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
根据该配置,401-409之间的状态码将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 40x"}
|
||||
```
|
||||
|
||||
### 老版本-不同状态码相同应答场景
|
||||
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 200
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
```
|
||||
根据该配置,200请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
@@ -12,30 +12,165 @@ Plugin Execution Phase: `Authentication Phase`
|
||||
Plugin Execution Priority: `910`
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | Optional | 200 | Custom HTTP response status code |
|
||||
| headers | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| body | string | Optional | - | Custom HTTP response body |
|
||||
| enable_on_status | array of number | Optional | - | Match original status codes to generate custom responses; if not specified, the original status code is not checked |
|
||||
### New version - Supports multiple returns
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|---------------------|-----------------|----------|-----|------------|
|
||||
| rules | array of object | Required | - | rule array |
|
||||
|
||||
The configuration field description of `rules` is as follows:
|
||||
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|--------------------|---------------------------|--------------|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | Optional | 200 | Custom HTTP response status code |
|
||||
| `headers` | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| `body` | string | Optional | - | Custom HTTP response body |
|
||||
| `enable_on_status` | array of string or number | Optional | - | Match the original status code to generate a custom response. You can fill in the exact value such as :`200`,`404`, etc., you can also fuzzy match such as: `2xx` to match the status code between 200-299, `20x` to match the status code between 200-209, x represents any digit. If enable_on_status is not specified, the original status code is not determined and the first rule with ENABLE_ON_status left blank is used as the default rule |
|
||||
|
||||
#### Fuzzy matching rule
|
||||
* Length is 3
|
||||
* At least one digit
|
||||
* At least one x(case insensitive)
|
||||
|
||||
| rule | Matching content |
|
||||
|------|------------------------------------------------------------------------------------------|
|
||||
| 40x | 400-409; If the first two digits are 40 |
|
||||
| 1x4 | 104,114,124,134,144,154,164,174,184,194;The first and third positions are 1 and 4 respectively |
|
||||
| x23 | 023,123,223,323,423,523,623,723,823,923;The second and third positions are 23 |
|
||||
| 4xx | 400-499;The first digit is 4 |
|
||||
| x4x | 040-049,140-149,240-249,340-349,440-449,540-549,640-649,740-749,840-849,940-949;The second digit is 4 |
|
||||
| xx4 | When the mantissa is 4 |
|
||||
|
||||
Matching priority: Exact Match > Fuzzy Match > Default configuration (the first enable_on_status parameter is null)
|
||||
|
||||
## Old version - Only one return is supported
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
| -------- | -------- | -------- | -------- |----------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | Optional | 200 | Custom HTTP response status code |
|
||||
| `headers` | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| `body` | string | Optional | - | Custom HTTP response body |
|
||||
| `enable_on_status` | array of number | Optional | - | Match original status codes to generate custom responses; if not specified, the original status code is not checked |
|
||||
|
||||
|
||||
## Configuration Example
|
||||
### Mock Response Scenario
|
||||
|
||||
### Different status codes for different response scenarios
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- '200'
|
||||
- '201'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 404"}'
|
||||
enable_on_status:
|
||||
- '404'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
With this configuration, the request will return the following custom response:
|
||||
|
||||
According to this configuration 200 201 requests will return a custom response as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
According to this configuration 404 requests will return a custom response as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 400"}
|
||||
```
|
||||
With this configuration, 404 response will return the following custom response:
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 404"}
|
||||
```
|
||||
|
||||
### Fuzzy matching scene
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 40x"}'
|
||||
enable_on_status:
|
||||
- '40x'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
According to this configuration, the status 200 will return a custom reply as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
According to this configuration, the status code between 401-409 will return a custom reply as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 40x"}
|
||||
```
|
||||
|
||||
### Mock Response Scenario
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 200
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
```
|
||||
With this configuration, 200/201 response will return the following custom response:
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
### Custom Response on Rate Limiting
|
||||
```yaml
|
||||
enable_on_status:
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
1.1.0
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.0.7
|
||||
entrypoint: /usr/local/bin/envoy
|
||||
# 注意这里对 Wasm 开启了 debug 级别日志,在生产环境部署时请使用默认的 info 级别
|
||||
# 如果需要将 Envoy 的日志级别调整为 debug,将 --log-level 参数设置为 debug
|
||||
command: -c /etc/envoy/envoy.yaml --log-level info --component-log-level wasm:debug
|
||||
depends_on:
|
||||
- echo-server
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
- "9901:9901"
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./plugin.wasm:/etc/envoy/plugin.wasm
|
||||
echo-server:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
wasmtest: {}
|
||||
@@ -1,9 +1,3 @@
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
@@ -27,9 +21,9 @@ static_resources:
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
prefix: "/echo"
|
||||
route:
|
||||
cluster: httpbin
|
||||
cluster: echo-server
|
||||
http_filters:
|
||||
- name: wasmdemo
|
||||
typed_config:
|
||||
@@ -42,30 +36,100 @@ static_resources:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/main.wasm
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
# value: |-
|
||||
# {
|
||||
# "rules": [
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "enable_on_status": [
|
||||
# 200,
|
||||
# 201
|
||||
# ],
|
||||
# "body": "{\"hello\":\"world 200\"}"
|
||||
# },
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "enable_on_status": [
|
||||
# 404
|
||||
# ],
|
||||
# "body": "{\"hello\":\"world 404\"}"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
value: |-
|
||||
{
|
||||
"headers": ["key1=value1", "key2=value2"],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [200, 201],
|
||||
"body": "{\"hello\":\"world\"}"
|
||||
"rules": [
|
||||
{
|
||||
"headers": [
|
||||
"key1=value1",
|
||||
"key2=value2"
|
||||
],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [
|
||||
200
|
||||
],
|
||||
"body": "{\"hello\":\"world 200\"}"
|
||||
},
|
||||
{
|
||||
"headers": [
|
||||
"key1=value1",
|
||||
"key2=value2"
|
||||
],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [
|
||||
"40x"
|
||||
],
|
||||
"body": "{\"hello\":\"world 40x\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
# value: |-
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "body": "{\"hello\":\"world 200\"}",
|
||||
# "enable_on_status": [
|
||||
# 200
|
||||
# ]
|
||||
# }
|
||||
# value: |-
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "body": "{\"hello\":\"world 200\"}"
|
||||
# }
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
- name: echo-server
|
||||
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
|
||||
cluster_name: echo-server
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 80
|
||||
address: echo-server
|
||||
port_value: 3000
|
||||
@@ -16,10 +16,12 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
@@ -29,78 +31,160 @@ import (
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"custom-response",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type CustomResponseConfig struct {
|
||||
rules []CustomResponseRule
|
||||
defaultRule *CustomResponseRule
|
||||
enableOnStatusRuleMap map[string]*CustomResponseRule
|
||||
}
|
||||
|
||||
type CustomResponseRule struct {
|
||||
statusCode uint32
|
||||
headers [][2]string
|
||||
body string
|
||||
enableOnStatus []uint32
|
||||
enableOnStatus []string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func parseConfig(gjson gjson.Result, config *CustomResponseConfig, log wrapper.Log) error {
|
||||
func parseConfig(gjson gjson.Result, config *CustomResponseConfig) error {
|
||||
rules := gjson.Get("rules")
|
||||
rulesVersion := rules.Exists() && rules.IsArray()
|
||||
if rulesVersion {
|
||||
for _, cf := range gjson.Get("rules").Array() {
|
||||
item := new(CustomResponseRule)
|
||||
if err := parseRuleItem(cf, item); err != nil {
|
||||
return err
|
||||
}
|
||||
// the first rule item which enableOnStatus is empty to be set default
|
||||
if len(item.enableOnStatus) == 0 && config.defaultRule == nil {
|
||||
config.defaultRule = item
|
||||
}
|
||||
config.rules = append(config.rules, *item)
|
||||
}
|
||||
} else {
|
||||
rule := new(CustomResponseRule)
|
||||
if err := parseRuleItem(gjson, rule); err != nil {
|
||||
return err
|
||||
}
|
||||
config.rules = append(config.rules, *rule)
|
||||
config.defaultRule = rule
|
||||
}
|
||||
config.enableOnStatusRuleMap = make(map[string]*CustomResponseRule)
|
||||
for i, configItem := range config.rules {
|
||||
for _, statusCode := range configItem.enableOnStatus {
|
||||
if v, ok := config.enableOnStatusRuleMap[statusCode]; ok {
|
||||
log.Errorf("enable_on_status code used in %v, want to add %v", v, statusCode)
|
||||
return errors.New("enableOnStatus can only use once")
|
||||
}
|
||||
config.enableOnStatusRuleMap[statusCode] = &config.rules[i]
|
||||
}
|
||||
}
|
||||
if rulesVersion && config.defaultRule == nil && len(config.enableOnStatusRuleMap) == 0 {
|
||||
return errors.New("no valid config is found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRuleItem(gjson gjson.Result, rule *CustomResponseRule) error {
|
||||
headersArray := gjson.Get("headers").Array()
|
||||
config.headers = make([][2]string, 0, len(headersArray))
|
||||
rule.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
|
||||
rule.contentType = value
|
||||
} else if strings.EqualFold(key, "content-length") {
|
||||
continue
|
||||
} else {
|
||||
config.headers = append(config.headers, [2]string{key, value})
|
||||
rule.headers = append(rule.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"
|
||||
rule.body = gjson.Get("body").String()
|
||||
if rule.contentType == "" && rule.body != "" {
|
||||
if json.Valid([]byte(rule.body)) {
|
||||
rule.contentType = "application/json; charset=utf-8"
|
||||
} else {
|
||||
config.contentType = "text/plain; charset=utf-8"
|
||||
rule.contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
}
|
||||
config.headers = append(config.headers, [2]string{"content-type", config.contentType})
|
||||
rule.headers = append(rule.headers, [2]string{"content-type", rule.contentType})
|
||||
|
||||
config.statusCode = 200
|
||||
rule.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)
|
||||
rule.statusCode = uint32(parsedStatusCode)
|
||||
}
|
||||
|
||||
enableOnStatusArray := gjson.Get("enable_on_status").Array()
|
||||
config.enableOnStatus = make([]uint32, 0, len(enableOnStatusArray))
|
||||
rule.enableOnStatus = make([]string, 0, len(enableOnStatusArray))
|
||||
for _, v := range enableOnStatusArray {
|
||||
parsedEnableOnStatus, err := strconv.Atoi(v.String())
|
||||
s := v.String()
|
||||
_, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid enable_on_status value: %s", v.String())
|
||||
matchString, err := isValidFuzzyMatchString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rule.enableOnStatus = append(rule.enableOnStatus, matchString)
|
||||
continue
|
||||
}
|
||||
config.enableOnStatus = append(config.enableOnStatus, uint32(parsedEnableOnStatus))
|
||||
rule.enableOnStatus = append(rule.enableOnStatus, s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||
if len(config.enableOnStatus) != 0 {
|
||||
func isValidFuzzyMatchString(s string) (string, error) {
|
||||
const requiredLength = 3
|
||||
if len(s) != requiredLength {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: length must be %d", s, requiredLength)
|
||||
}
|
||||
|
||||
lower := strings.ToLower(s)
|
||||
hasX := false
|
||||
hasDigit := false
|
||||
|
||||
for _, c := range lower {
|
||||
switch {
|
||||
case c == 'x':
|
||||
hasX = true
|
||||
case c >= '0' && c <= '9':
|
||||
hasDigit = true
|
||||
default:
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: must contain only digits and x/X", s)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasX {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: fuzzy match must contain x/X (use enable_on_status for exact statusCode matching)", s)
|
||||
}
|
||||
if !hasDigit {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: must contain at least one digit", s)
|
||||
}
|
||||
|
||||
return lower, nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(_ wrapper.HttpContext, config CustomResponseConfig) types.Action {
|
||||
if len(config.enableOnStatusRuleMap) != 0 {
|
||||
return types.ActionContinue
|
||||
}
|
||||
err := proxywasm.SendHttpResponseWithDetail(config.statusCode, "custom-response", config.headers, []byte(config.body), -1)
|
||||
log.Infof("use default rule %+v", config.defaultRule)
|
||||
err := proxywasm.SendHttpResponseWithDetail(config.defaultRule.statusCode, "custom-response", config.defaultRule.headers, []byte(config.defaultRule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
@@ -108,28 +192,62 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig,
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||
// enableOnStatus is not empty, compare the status code.
|
||||
func onHttpResponseHeaders(_ wrapper.HttpContext, config CustomResponseConfig) types.Action {
|
||||
// enableOnStatusRuleMap 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)
|
||||
if rule, ok := config.enableOnStatusRuleMap[statusCodeStr]; ok {
|
||||
err = proxywasm.SendHttpResponseWithDetail(rule.statusCode, "custom-response", rule.headers, []byte(rule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
for _, v := range config.enableOnStatus {
|
||||
if uint32(statusCode) == v {
|
||||
err = proxywasm.SendHttpResponseWithDetail(config.statusCode, "custom-response", config.headers, []byte(config.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
if rule, match := fuzzyMatchCode(config.enableOnStatusRuleMap, statusCodeStr); match {
|
||||
err = proxywasm.SendHttpResponseWithDetail(rule.statusCode, "custom-response", rule.headers, []byte(rule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func fuzzyMatchCode(statusRuleMap map[string]*CustomResponseRule, statusCode string) (*CustomResponseRule, bool) {
|
||||
if len(statusRuleMap) == 0 || statusCode == "" {
|
||||
return nil, false
|
||||
}
|
||||
codeLen := len(statusCode)
|
||||
for pattern, rule := range statusRuleMap {
|
||||
// 规则1:模式长度必须与状态码一致
|
||||
if len(pattern) != codeLen {
|
||||
continue
|
||||
}
|
||||
// 纯数字的enableOnStatus已经判断过,跳过
|
||||
if !strings.Contains(pattern, "x") {
|
||||
continue
|
||||
}
|
||||
// 规则2:所有数字位必须精确匹配
|
||||
match := true
|
||||
for i, c := range pattern {
|
||||
// 如果是数字位需要校验
|
||||
if c >= '0' && c <= '9' {
|
||||
// 边界检查防止panic
|
||||
if i >= codeLen || statusCode[i] != byte(c) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// 非数字位(如x)自动匹配
|
||||
}
|
||||
if match {
|
||||
return rule, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
80
plugins/wasm-go/extensions/custom-response/main_test.go
Normal file
80
plugins/wasm-go/extensions/custom-response/main_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_prefixMatchCode(t *testing.T) {
|
||||
rules := map[string]*CustomResponseRule{
|
||||
"x01": {},
|
||||
"2x3": {},
|
||||
"45x": {},
|
||||
"6xx": {},
|
||||
"x7x": {},
|
||||
"xx8": {},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
code string
|
||||
expectHit bool
|
||||
}{
|
||||
{"101", true}, // 匹配x01
|
||||
{"201", true}, // 匹配x01
|
||||
{"111", false}, // 不匹配
|
||||
{"203", true}, // 匹配2x3
|
||||
{"213", true}, // 匹配2x3
|
||||
{"450", true}, // 匹配45x
|
||||
{"451", true}, // 匹配45x
|
||||
{"600", true}, // 匹配6xx
|
||||
{"611", true}, // 匹配6xx
|
||||
{"612", true}, // 匹配6xx
|
||||
{"171", true}, // 匹配x7x
|
||||
{"161", false}, // 不匹配
|
||||
{"228", true}, // 匹配xx8
|
||||
{"229", false}, // 不匹配
|
||||
{"123", false}, // 不匹配
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, found := fuzzyMatchCode(rules, tt.code)
|
||||
if found != tt.expectHit {
|
||||
t.Errorf("code:%s expect:%v got:%v", tt.code, tt.expectHit, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidPrefixString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
hasError bool
|
||||
}{
|
||||
{"x1x", "x1x", false},
|
||||
{"X2X", "x2x", false},
|
||||
{"xx1", "xx1", false},
|
||||
{"x12", "x12", false},
|
||||
{"1x2", "1x2", false},
|
||||
{"12x", "12x", false},
|
||||
{"123", "", true}, // 缺少x
|
||||
{"xxx", "", true}, // 缺少数字
|
||||
{"xYx", "", true}, // 非法字符
|
||||
{"x1", "", true}, // 长度不足
|
||||
{"x123", "", true}, // 长度超限
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result, err := isValidFuzzyMatchString(tt.input)
|
||||
if tt.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("%q: expected error but got none", tt.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error: %v", tt.input, err)
|
||||
}
|
||||
if result != tt.expected {
|
||||
t.Errorf("%q: expected %q, got %q", tt.input, tt.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user