mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 15:10:54 +08:00
feat: Add traffic-editor plugin (#2825)
This commit is contained in:
206
plugins/wasm-go/extensions/traffic-editor/README.md
Normal file
206
plugins/wasm-go/extensions/traffic-editor/README.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: 请求响应编辑
|
||||
keywords: [ higress,request,response,edit ]
|
||||
description: 请求响应编辑插件使用说明
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
|
||||
`traffic-editor` 插件可以对请求/响应头进行修改,支持的修改操作类型包括删除、重命名、更新、添加、追加、映射、去重。
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`100`
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------------------|-----------------------------------------|----|---------------------|
|
||||
| defaultConfig | object (CommandSet) | 否 | 默认命令集配置,无条件执行的编辑操作 |
|
||||
| conditionalConfigs | array of object (ConditionalCommandSet) | 否 | 条件命令集配置,按条件执行不同编辑操作 |
|
||||
|
||||
### CommandSet 结构
|
||||
|
||||
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|----------------|---------------------------|----|-------|------------|
|
||||
| disableReroute | bool | 否 | false | 是否禁用自动路由重选 |
|
||||
| commands | array of object (Command) | 是 | - | 编辑命令列表 |
|
||||
|
||||
### ConditionalCommandSet 结构
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|------------|---------------------------|----|---------------------|
|
||||
| conditions | array | 是 | 条件列表,见下表 |
|
||||
| commands | array of object (Command) | 是 | 命令列表,结构同 CommandSet |
|
||||
|
||||
#### Command 结构
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|------|--------|----|-------------------|
|
||||
| type | string | 是 | 命令类型。其他配置字段由类型决定。 |
|
||||
|
||||
##### set 命令
|
||||
|
||||
功能为将某个字段设置为指定值。`type` 字段值为 `set`。
|
||||
|
||||
其它字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|--------|
|
||||
| target | object (Ref) | 是 | 目标字段信息 |
|
||||
| value | string | 是 | 要设置的值 |
|
||||
|
||||
##### concat 命令
|
||||
|
||||
功能为将多个值拼接后赋值给目标字段。`type` 字段值为 `concat`。
|
||||
|
||||
其它字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|-----------------------|----|--------------------------|
|
||||
| target | object (Ref) | 是 | 目标字段信息 |
|
||||
| values | array of (string/Ref) | 是 | 要拼接的值列表,可以是字符串或字段引用(Ref) |
|
||||
|
||||
##### copy 命令
|
||||
|
||||
功能为将源字段的值复制到目标字段。`type` 字段值为 `copy`。
|
||||
|
||||
其它字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|--------|
|
||||
| source | object (Ref) | 是 | 源字段信息 |
|
||||
| target | object (Ref) | 是 | 目标字段信息 |
|
||||
|
||||
##### delete 命令
|
||||
|
||||
功能为删除指定字段。`type` 字段值为 `delete`。
|
||||
|
||||
其它字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|----------|
|
||||
| target | object (Ref) | 是 | 要删除的字段信息 |
|
||||
|
||||
##### rename 命令
|
||||
|
||||
功能为将字段重命名。`type` 字段值为 `rename`。
|
||||
|
||||
其它字段如下:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|-------|
|
||||
| source | object (Ref) | 是 | 原字段信息 |
|
||||
| target | object (Ref) | 是 | 新字段信息 |
|
||||
|
||||
#### Condition 结构
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|------|--------|----|-------------------|
|
||||
| type | string | 是 | 条件类型。其他配置字段由类型决定。 |
|
||||
|
||||
##### equals 条件
|
||||
|
||||
判断某字段值是否等于指定值。`type` 字段值为 `equals`。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|---------|
|
||||
| value1 | object (Ref) | 是 | 参与比较的字段 |
|
||||
| value2 | string | 是 | 目标值 |
|
||||
|
||||
##### prefix 条件
|
||||
|
||||
判断某字段值是否以指定前缀开头。`type` 字段值为 `prefix`。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|---------|
|
||||
| value | object (Ref) | 是 | 参与比较的字段 |
|
||||
| prefix | string | 是 | 前缀字符串 |
|
||||
|
||||
##### suffix 条件
|
||||
|
||||
判断某字段值是否以指定后缀结尾。`type` 字段值为 `suffix`。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|---------|
|
||||
| value | object (Ref) | 是 | 参与比较的字段 |
|
||||
| suffix | string | 是 | 后缀字符串 |
|
||||
|
||||
##### contains 条件
|
||||
|
||||
判断某字段值是否包含指定子串。`type` 字段值为 `contains`。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------------|----|---------|
|
||||
| value | object (Ref) | 是 | 参与比较的字段 |
|
||||
| substr | string | 是 | 子串 |
|
||||
|
||||
##### regex 条件
|
||||
|
||||
判断某字段值是否匹配指定正则表达式。`type` 字段值为 `regex`。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|---------|--------------|----|---------|
|
||||
| value | object (Ref) | 是 | 参与比较的字段 |
|
||||
| pattern | string | 是 | 正则表达式 |
|
||||
|
||||
#### Ref 结构
|
||||
|
||||
用于标识一个请求或响应中的字段。
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|------|--------|----|--------------------------------------------------------------|
|
||||
| type | string | 是 | 字段类型。可选值有:`request_header`、`request_query`、`response_header` |
|
||||
| name | string | 是 | 字段名称 |
|
||||
|
||||
### 示例配置
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultConfig": {
|
||||
"disableReroute": false,
|
||||
"commands": [
|
||||
{
|
||||
"type": "set",
|
||||
"target": {
|
||||
"type": "request_header",
|
||||
"name": "x-user"
|
||||
},
|
||||
"value": "admin"
|
||||
},
|
||||
{
|
||||
"type": "delete",
|
||||
"target": {
|
||||
"type": "request_header",
|
||||
"name": "x-dummy"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"type": "equals",
|
||||
"value1": {
|
||||
"type": "request_query",
|
||||
"name": "id"
|
||||
},
|
||||
"value2": "1"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"type": "set",
|
||||
"target": {
|
||||
"type": "response_header",
|
||||
"name": "x-id"
|
||||
},
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
212
plugins/wasm-go/extensions/traffic-editor/README_EN.md
Normal file
212
plugins/wasm-go/extensions/traffic-editor/README_EN.md
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Request/Response Editor
|
||||
keywords: [higress,request,response,edit]
|
||||
description: Usage guide for the request/response editor plugin
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
The `traffic-editor` plugin allows you to modify request/response headers. Supported operations include delete, rename, update, add, append, map, and deduplicate.
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
Plugin execution phase: `UNSPECIFIED`
|
||||
Plugin execution priority: `100`
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|--------------------|-------------------------------------------|----------|-----------------------------------|
|
||||
| defaultConfig | object (CommandSet) | No | Default command set, executed unconditionally |
|
||||
| conditionalConfigs | array of object (ConditionalCommandSet) | No | Conditional command sets, executed based on conditions |
|
||||
|
||||
### CommandSet Structure
|
||||
|
||||
| Field Name | Type | Required | Default | Description |
|
||||
|----------------|----------------------------|----------|---------|----------------------------|
|
||||
| disableReroute | bool | No | false | Whether to disable automatic route selection |
|
||||
| commands | array of object (Command) | Yes | - | List of edit commands |
|
||||
|
||||
### ConditionalCommandSet Structure
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|-------------|----------------------------|----------|-----------------------------------|
|
||||
| conditions | array | Yes | List of conditions, see below |
|
||||
| commands | array of object (Command) | Yes | List of commands, same as CommandSet |
|
||||
|
||||
#### Command Structure
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|--------|----------|------------------------------|
|
||||
| type | string | Yes | Command type, other fields depend on type |
|
||||
|
||||
##### set Command
|
||||
|
||||
Sets a field to a specified value. `type` field value is `set`.
|
||||
|
||||
Other fields:
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| target | object (Ref) | Yes | Target field info|
|
||||
| value | string | Yes | Value to set |
|
||||
|
||||
##### concat Command
|
||||
|
||||
Concatenates multiple values and assigns to the target field. `type` field value is `concat`.
|
||||
|
||||
Other fields:
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|-----------------------|----------|----------------------------------------------|
|
||||
| target | object (Ref) | Yes | Target field info |
|
||||
| values | array of (string/Ref) | Yes | Values to concatenate, can be string or Ref |
|
||||
|
||||
##### copy Command
|
||||
|
||||
Copies the value from the source field to the target field. `type` field value is `copy`.
|
||||
|
||||
Other fields:
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| source | object (Ref) | Yes | Source field info|
|
||||
| target | object (Ref) | Yes | Target field info|
|
||||
|
||||
##### delete Command
|
||||
|
||||
Deletes the specified field. `type` field value is `delete`.
|
||||
|
||||
Other fields:
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| target | object (Ref) | Yes | Field to delete |
|
||||
|
||||
##### rename Command
|
||||
|
||||
Renames a field. `type` field value is `rename`.
|
||||
|
||||
Other fields:
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| source | object (Ref) | Yes | Original field info|
|
||||
| target | object (Ref) | Yes | New field info |
|
||||
|
||||
#### Condition Structure
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|--------|----------|------------------------------|
|
||||
| type | string | Yes | Condition type, other fields depend on type |
|
||||
|
||||
##### equals Condition
|
||||
|
||||
Checks if a field value equals the specified value. `type` field value is `equals`.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| value1 | object (Ref) | Yes | Field to compare |
|
||||
| value2 | string | Yes | Target value |
|
||||
|
||||
##### prefix Condition
|
||||
|
||||
Checks if a field value starts with the specified prefix. `type` field value is `prefix`.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| value | object (Ref) | Yes | Field to compare |
|
||||
| prefix | string | Yes | Prefix string |
|
||||
|
||||
##### suffix Condition
|
||||
|
||||
Checks if a field value ends with the specified suffix. `type` field value is `suffix`.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| value | object (Ref) | Yes | Field to compare |
|
||||
| suffix | string | Yes | Suffix string |
|
||||
|
||||
##### contains Condition
|
||||
|
||||
Checks if a field value contains the specified substring. `type` field value is `contains`.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| value | object (Ref) | Yes | Field to compare |
|
||||
| substr | string | Yes | Substring |
|
||||
|
||||
##### regex Condition
|
||||
|
||||
Checks if a field value matches the specified regular expression. `type` field value is `regex`.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|---------------|----------|------------------|
|
||||
| value | object (Ref) | Yes | Field to compare |
|
||||
| pattern | string | Yes | Regular expression|
|
||||
|
||||
#### Ref Structure
|
||||
|
||||
Used to identify a field in the request or response.
|
||||
|
||||
| Field Name | Type | Required | Description |
|
||||
|------------|--------|----------|------------------------------------------------------------------|
|
||||
| type | string | Yes | Field type: `request_header`, `request_query`, `response_header` |
|
||||
| name | string | Yes | Field name |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultConfig": {
|
||||
"disableReroute": false,
|
||||
"commands": [
|
||||
{ "type": "set", "target": { "type": "request_header", "name": "x-user" }, "value": "admin" },
|
||||
{ "type": "delete", "target": { "type": "request_header", "name": "x-remove" } }
|
||||
]
|
||||
},
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{ "type": "equals", "value1": { "type": "request_query", "name": "role" }, "value2": "admin" }
|
||||
],
|
||||
"commands": [
|
||||
{ "type": "set", "target": { "type": "response_header", "name": "x-status" }, "value": "is-admin" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{ "type": "prefix", "value": { "type": "request_header", "name": "x-path" }, "prefix": "/api/" }
|
||||
],
|
||||
"commands": [
|
||||
{ "type": "rename", "source": { "type": "request_header", "name": "x-old" }, "target": { "type": "request_header", "name": "x-new" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{ "type": "suffix", "value": { "type": "request_header", "name": "x-path" }, "suffix": ".json" }
|
||||
],
|
||||
"commands": [
|
||||
{ "type": "copy", "source": { "type": "request_query", "name": "id" }, "target": { "type": "response_header", "name": "x-id" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{ "type": "contains", "value": { "type": "request_header", "name": "x-info" }, "substr": "test" }
|
||||
],
|
||||
"commands": [
|
||||
{ "type": "concat", "target": { "type": "response_header", "name": "x-token" }, "values": ["prefix-", { "type": "request_query", "name": "token" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{ "type": "regex", "value": { "type": "request_query", "name": "email" }, "pattern": "^.+@example\\.com$" }
|
||||
],
|
||||
"commands": [
|
||||
{ "type": "delete", "target": { "type": "response_header", "name": "x-temp" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1
plugins/wasm-go/extensions/traffic-editor/VERSION
Normal file
1
plugins/wasm-go/extensions/traffic-editor/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0-alpha
|
||||
37
plugins/wasm-go/extensions/traffic-editor/config.go
Normal file
37
plugins/wasm-go/extensions/traffic-editor/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/traffic-editor/pkg"
|
||||
)
|
||||
|
||||
type PluginConfig struct {
|
||||
DefaultConfig *pkg.CommandSet `json:"defaultConfig,omitempty"`
|
||||
ConditionalConfigs []*pkg.ConditionalCommandSet `json:"conditionalConfigs,omitempty"`
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result) error {
|
||||
c.DefaultConfig = nil
|
||||
defaultConfigJson := json.Get("defaultConfig")
|
||||
if defaultConfigJson.Exists() && defaultConfigJson.IsObject() {
|
||||
c.DefaultConfig = &pkg.CommandSet{}
|
||||
if err := c.DefaultConfig.FromJson(defaultConfigJson); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.ConditionalConfigs = nil
|
||||
conditionalConfigsJson := json.Get("conditionalConfigs")
|
||||
if conditionalConfigsJson.Exists() && conditionalConfigsJson.IsArray() {
|
||||
for _, item := range conditionalConfigsJson.Array() {
|
||||
config := &pkg.ConditionalCommandSet{}
|
||||
if err := config.FromJson(item); err != nil {
|
||||
return err
|
||||
}
|
||||
c.ConditionalConfigs = append(c.ConditionalConfigs, config)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.6
|
||||
entrypoint: /usr/local/bin/envoy
|
||||
# 注意这里对wasm开启了debug级别日志,正式部署时则默认info级别
|
||||
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||
#depends_on:
|
||||
# - httpbin
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./plugin.wasm:/etc/envoy/main.wasm
|
||||
|
||||
httpbin:
|
||||
image: kong/httpbin:latest
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "12345:80"
|
||||
|
||||
networks:
|
||||
wasmtest: {}
|
||||
24
plugins/wasm-go/extensions/traffic-editor/go.mod
Normal file
24
plugins/wasm-go/extensions/traffic-editor/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/traffic-editor
|
||||
|
||||
go 1.24.1
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
31
plugins/wasm-go/extensions/traffic-editor/go.sum
Normal file
31
plugins/wasm-go/extensions/traffic-editor/go.sum
Normal file
@@ -0,0 +1,31 @@
|
||||
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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2 h1:8fQqR+wHts8tP+v7GYxmsCNyW5nAjn9wPYV0/+Seqzg=
|
||||
github.com/higress-group/wasm-go v1.0.2/go.mod h1:882/J8ccU4i+LeyFKmeicbHWAYLj8y7YZr60zk0OOCI=
|
||||
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
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=
|
||||
22
plugins/wasm-go/extensions/traffic-editor/http.go
Normal file
22
plugins/wasm-go/extensions/traffic-editor/http.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
func headerSlice2Map(headerSlice [][2]string) map[string][]string {
|
||||
headerMap := make(map[string][]string)
|
||||
for _, header := range headerSlice {
|
||||
k, v := strings.ToLower(header[0]), header[1]
|
||||
headerMap[k] = append(headerMap[k], v)
|
||||
}
|
||||
return headerMap
|
||||
}
|
||||
|
||||
func headerMap2Slice(headerMap map[string][]string) [][2]string {
|
||||
headerSlice := make([][2]string, 0, len(headerMap))
|
||||
for k, vs := range headerMap {
|
||||
for _, v := range vs {
|
||||
headerSlice = append(headerSlice, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
return headerSlice
|
||||
}
|
||||
177
plugins/wasm-go/extensions/traffic-editor/main.go
Normal file
177
plugins/wasm-go/extensions/traffic-editor/main.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/traffic-editor/pkg"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
const (
|
||||
ctxKeyEditorContext = "editorContext"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"traffic-editor",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *PluginConfig) (err error) {
|
||||
if err := config.FromJson(json); err != nil {
|
||||
return fmt.Errorf("failed to parse plugin config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||
log.Debugf("onHttpRequestHeaders called with config")
|
||||
|
||||
editorContext := pkg.NewEditorContext()
|
||||
if headers, err := proxywasm.GetHttpRequestHeaders(); err == nil {
|
||||
editorContext.SetRequestHeaders(headerSlice2Map(headers))
|
||||
} else {
|
||||
log.Errorf("failed to get request headers: %v", err)
|
||||
}
|
||||
saveEditorContext(ctx, editorContext)
|
||||
|
||||
effectiveCommandSet := findEffectiveCommandSet(editorContext, &config)
|
||||
if effectiveCommandSet == nil {
|
||||
log.Debugf("no effective command set found for request %s", ctx.Path())
|
||||
return types.ActionContinue
|
||||
}
|
||||
if len(effectiveCommandSet.Commands) == 0 {
|
||||
log.Debugf("the effective command set found for request %s is empty", ctx.Path())
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
log.Debugf("an effective command set found for request %s with %d commands", ctx.Path(), len(effectiveCommandSet.Commands))
|
||||
editorContext.SetEffectiveCommandSet(effectiveCommandSet)
|
||||
editorContext.SetCommandExecutors(effectiveCommandSet.CreatExecutors())
|
||||
|
||||
// Make sure the editor context is clean before executing any command.
|
||||
editorContext.ResetDirtyFlags()
|
||||
|
||||
if effectiveCommandSet.DisableReroute {
|
||||
ctx.DisableReroute()
|
||||
}
|
||||
|
||||
executeCommands(editorContext, pkg.StageRequestHeaders)
|
||||
|
||||
if err := saveRequestHeaderChanges(editorContext); err != nil {
|
||||
log.Errorf("failed to save request header changes: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the editor context is clean before continue.
|
||||
editorContext.ResetDirtyFlags()
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||
log.Debugf("onHttpResponseHeaders called with config")
|
||||
|
||||
editorContext := loadEditorContext(ctx)
|
||||
if editorContext.GetEffectiveCommandSet() == nil {
|
||||
log.Debugf("no effective command set found for request %s", ctx.Path())
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if headers, err := proxywasm.GetHttpResponseHeaders(); err == nil {
|
||||
editorContext.SetResponseHeaders(headerSlice2Map(headers))
|
||||
} else {
|
||||
log.Errorf("failed to get response headers: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the editor context is clean before executing any command.
|
||||
editorContext.ResetDirtyFlags()
|
||||
|
||||
executeCommands(editorContext, pkg.StageResponseHeaders)
|
||||
if err := saveResponseHeaderChanges(editorContext); err != nil {
|
||||
log.Errorf("failed to save response header changes: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the editor context is clean before continue.
|
||||
editorContext.ResetDirtyFlags()
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func findEffectiveCommandSet(editorContext pkg.EditorContext, config *PluginConfig) *pkg.CommandSet {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
if len(config.ConditionalConfigs) != 0 {
|
||||
for i, conditionalConfig := range config.ConditionalConfigs {
|
||||
log.Debugf("Evaluating conditional config %d: %+v", i, conditionalConfig)
|
||||
if conditionalConfig.Matches(editorContext) {
|
||||
log.Debugf("Use the conditional command set %d", i)
|
||||
return &conditionalConfig.CommandSet
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("Use the default command set")
|
||||
return config.DefaultConfig
|
||||
}
|
||||
|
||||
func executeCommands(editorContext pkg.EditorContext, stage pkg.Stage) {
|
||||
for _, executor := range editorContext.GetCommandExecutors() {
|
||||
if err := executor.Run(editorContext, stage); err != nil {
|
||||
log.Errorf("failed to execute a %s command in stage %s: %v", executor.GetCommand().GetType(), pkg.Stage2String[stage], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveRequestHeaderChanges(editorContext pkg.EditorContext) error {
|
||||
if !editorContext.IsRequestHeadersDirty() {
|
||||
log.Debugf("no request header change to save")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("saving request header changes: %v", editorContext.GetRequestHeaders())
|
||||
headerSlice := headerMap2Slice(editorContext.GetRequestHeaders())
|
||||
return proxywasm.ReplaceHttpRequestHeaders(headerSlice)
|
||||
}
|
||||
|
||||
func saveResponseHeaderChanges(editorContext pkg.EditorContext) error {
|
||||
if !editorContext.IsResponseHeadersDirty() {
|
||||
log.Debugf("no response header change to save")
|
||||
return nil
|
||||
}
|
||||
log.Debugf("saving response header changes: %v", editorContext.GetResponseHeaders())
|
||||
headerSlice := headerMap2Slice(editorContext.GetResponseHeaders())
|
||||
return proxywasm.ReplaceHttpResponseHeaders(headerSlice)
|
||||
}
|
||||
|
||||
func loadEditorContext(ctx wrapper.HttpContext) pkg.EditorContext {
|
||||
editorContext, _ := ctx.GetContext(ctxKeyEditorContext).(pkg.EditorContext)
|
||||
return editorContext
|
||||
}
|
||||
|
||||
func saveEditorContext(ctx wrapper.HttpContext, editorContext pkg.EditorContext) {
|
||||
ctx.SetContext(ctxKeyEditorContext, editorContext)
|
||||
}
|
||||
306
plugins/wasm-go/extensions/traffic-editor/main_test.go
Normal file
306
plugins/wasm-go/extensions/traffic-editor/main_test.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSample(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("default config only", func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`
|
||||
{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{
|
||||
"type": "set",
|
||||
"target": {
|
||||
"type": "request_header",
|
||||
"name": "x-test"
|
||||
},
|
||||
"value": "123456"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
expectedNewHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"x-test", "123456"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
newHeaders := host.GetRequestHeaders()
|
||||
require.True(t, compareHeaders(expectedNewHeaders, newHeaders), "expected headers: %v, got: %v", expectedNewHeaders, newHeaders)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetMultipleRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-a"}, "value": "aaa"},
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-b"}, "value": "bbb"}
|
||||
]
|
||||
}
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
originalHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-c", "ccc"},
|
||||
}
|
||||
action := host.CallOnHttpRequestHeaders(originalHeaders)
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
expectedHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-a", "aaa"},
|
||||
{"x-b", "bbb"},
|
||||
{"x-c", "ccc"},
|
||||
}
|
||||
newHeaders := host.GetRequestHeaders()
|
||||
require.True(t, compareHeaders(expectedHeaders, newHeaders))
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionalConfigMatch(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-def"}, "value": "default"}
|
||||
]
|
||||
},
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{"type": "equals", "value1": {"type": "request_header", "name": "x-cond"}, "value2": "match"}
|
||||
],
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-special"}, "value": "special"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
originalHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/data"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-cond", "match"},
|
||||
}
|
||||
action := host.CallOnHttpRequestHeaders(originalHeaders)
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
expectedHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/data"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-cond", "match"},
|
||||
{"x-special", "special"},
|
||||
}
|
||||
newHeaders := host.GetRequestHeaders()
|
||||
require.True(t, compareHeaders(expectedHeaders, newHeaders))
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionalConfigNoMatch(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-def"}, "value": "default"}
|
||||
]
|
||||
},
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{"type": "equals", "value1": {"type": "request_header", "name": "x-cond"}, "value2": "match"}
|
||||
],
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_header", "name": "x-special"}, "value": "special"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
originalHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-cond", "notmatch"},
|
||||
}
|
||||
action := host.CallOnHttpRequestHeaders(originalHeaders)
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
expectedHeaders := [][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/get"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"x-cond", "notmatch"},
|
||||
{"x-def", "default"},
|
||||
}
|
||||
newHeaders := host.GetRequestHeaders()
|
||||
require.True(t, compareHeaders(expectedHeaders, newHeaders))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetResponseHeader(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "response_header", "name": "x-res"}, "value": "respval"}
|
||||
]
|
||||
}
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{{"x-origin", "originval"}})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
newHeaders := host.GetResponseHeaders()
|
||||
require.True(t, compareHeaders([][2]string{{"x-origin", "originval"}, {"x-res", "respval"}}, newHeaders))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPathQueryParseAndHeaderChange(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"defaultConfig": {
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "request_query", "name": "foo"}, "value": "bar"}
|
||||
]
|
||||
}
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{":path", "/get?foo=old&baz=1"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
newHeaders := host.GetRequestHeaders()
|
||||
found := false
|
||||
for _, h := range newHeaders {
|
||||
if h[0] == ":path" && strings.Contains(h[1], "foo=bar") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "path header should be updated with foo=bar")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionSetMultiStage(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{"type": "equals", "value1": {"type": "request_header", "name": "x-a"}, "value2": "aaa"}
|
||||
],
|
||||
"commands": [
|
||||
{"type": "set", "target": {"type": "response_header", "name": "x-b"}, "value": "bbb"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
actionReq := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{":path", "/get?foo=old&baz=1"},
|
||||
{"x-a", "aaa"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, actionReq)
|
||||
actionResp := host.CallOnHttpResponseHeaders([][2]string{{"content-type", "application/json"}})
|
||||
require.Equal(t, types.ActionContinue, actionResp)
|
||||
newHeaders := host.GetResponseHeaders()
|
||||
require.True(t, compareHeaders([][2]string{{"x-b", "bbb"}, {"content-type", "application/json"}}, newHeaders))
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionSetMultiStage2(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost([]byte(`{
|
||||
"conditionalConfigs": [
|
||||
{
|
||||
"conditions": [
|
||||
{"type": "equals", "value1": {"type": "request_header", "name": "x-a"}, "value2": "aaa"}
|
||||
],
|
||||
"commands": [
|
||||
{"type": "copy", "source": {"type": "request_header", "name": "x-b"}, "target": {"type": "response_header", "name": "x-c"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
actionReq := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{":path", "/get?foo=old&baz=1"},
|
||||
{"x-a", "aaa"},
|
||||
{"x-b", "bbb"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, actionReq)
|
||||
actionResp := host.CallOnHttpResponseHeaders([][2]string{{"content-type", "application/json"}})
|
||||
require.Equal(t, types.ActionContinue, actionResp)
|
||||
newHeaders := host.GetResponseHeaders()
|
||||
require.True(t, compareHeaders([][2]string{{"x-c", "bbb"}, {"content-type", "application/json"}}, newHeaders))
|
||||
})
|
||||
}
|
||||
func compareHeaders(headers1, headers2 [][2]string) bool {
|
||||
if len(headers1) != len(headers2) {
|
||||
return false
|
||||
}
|
||||
m1 := make(map[string]string, len(headers1))
|
||||
m2 := make(map[string]string, len(headers2))
|
||||
for _, h := range headers1 {
|
||||
m1[strings.ToLower(h[0])] = h[1]
|
||||
}
|
||||
for _, h := range headers2 {
|
||||
m2[strings.ToLower(h[0])] = h[1]
|
||||
}
|
||||
if len(m1) != len(m2) {
|
||||
return false
|
||||
}
|
||||
for k, v := range m1 {
|
||||
if mv, ok := m2[k]; !ok || mv != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
515
plugins/wasm-go/extensions/traffic-editor/pkg/command.go
Normal file
515
plugins/wasm-go/extensions/traffic-editor/pkg/command.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
commandTypeSet = "set"
|
||||
commandTypeConcat = "concat"
|
||||
commandTypeCopy = "copy"
|
||||
commandTypeDelete = "delete"
|
||||
commandTypeRename = "rename"
|
||||
)
|
||||
|
||||
var (
|
||||
commandFactories = map[string]func(gjson.Result) (Command, error){
|
||||
"set": newSetCommand,
|
||||
"concat": newConcatCommand,
|
||||
"copy": newCopyCommand,
|
||||
"delete": newDeleteCommand,
|
||||
"rename": newRenameCommand,
|
||||
}
|
||||
)
|
||||
|
||||
type CommandSet struct {
|
||||
DisableReroute bool `json:"disableReroute"`
|
||||
Commands []Command `json:"commands,omitempty"`
|
||||
RelatedStages map[Stage]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *CommandSet) FromJson(json gjson.Result) error {
|
||||
relatedStages := map[Stage]bool{}
|
||||
if commandsJson := json.Get("commands"); commandsJson.Exists() && commandsJson.IsArray() {
|
||||
for _, item := range commandsJson.Array() {
|
||||
if command, err := NewCommand(item); err != nil {
|
||||
return fmt.Errorf("failed to create command from json: %v\n %v", err, item)
|
||||
} else {
|
||||
s.Commands = append(s.Commands, command)
|
||||
for _, ref := range command.GetRefs() {
|
||||
relatedStages[ref.GetStage()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.RelatedStages = relatedStages
|
||||
if disableReroute := json.Get("disableReroute"); disableReroute.Exists() {
|
||||
s.DisableReroute = disableReroute.Bool()
|
||||
} else {
|
||||
s.DisableReroute = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommandSet) CreatExecutors() []Executor {
|
||||
executors := make([]Executor, 0, len(s.Commands))
|
||||
for _, command := range s.Commands {
|
||||
executor := command.CreateExecutor()
|
||||
executors = append(executors, executor)
|
||||
}
|
||||
return executors
|
||||
}
|
||||
|
||||
type ConditionalCommandSet struct {
|
||||
ConditionSet
|
||||
CommandSet
|
||||
}
|
||||
|
||||
func (s *ConditionalCommandSet) FromJson(json gjson.Result) error {
|
||||
if err := s.ConditionSet.FromJson(json); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.CommandSet.FromJson(json); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Command interface {
|
||||
GetType() string
|
||||
GetRefs() []*Ref
|
||||
CreateExecutor() Executor
|
||||
}
|
||||
|
||||
type Executor interface {
|
||||
GetCommand() Command
|
||||
Run(editorContext EditorContext, stage Stage) error
|
||||
}
|
||||
|
||||
func NewCommand(json gjson.Result) (Command, error) {
|
||||
t := json.Get("type").String()
|
||||
if t == "" {
|
||||
return nil, errors.New("command type is required")
|
||||
}
|
||||
if constructor, ok := commandFactories[t]; ok && constructor != nil {
|
||||
return constructor(json)
|
||||
} else {
|
||||
return nil, errors.New("unknown command type: " + t)
|
||||
}
|
||||
}
|
||||
|
||||
type baseExecutor struct {
|
||||
finished bool
|
||||
}
|
||||
|
||||
// setCommand
|
||||
func newSetCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("setCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
var value string
|
||||
if v := json.Get("value"); !v.Exists() {
|
||||
return nil, errors.New("setCommand: value field is required")
|
||||
} else {
|
||||
value = v.String()
|
||||
if value == "" {
|
||||
return nil, errors.New("setCommand: value cannot be empty")
|
||||
}
|
||||
}
|
||||
return &setCommand{
|
||||
targetRef: targetRef,
|
||||
value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type setCommand struct {
|
||||
targetRef *Ref
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *setCommand) GetType() string {
|
||||
return commandTypeSet
|
||||
}
|
||||
|
||||
func (c *setCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *setCommand) CreateExecutor() Executor {
|
||||
return &setExecutor{command: c}
|
||||
}
|
||||
|
||||
type setExecutor struct {
|
||||
baseExecutor
|
||||
command *setCommand
|
||||
}
|
||||
|
||||
func (e *setExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *setExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("setCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
if command.targetRef.GetStage() == stage {
|
||||
log.Debugf("setCommand: set %s to %s", command.targetRef, command.value)
|
||||
editorContext.SetRefValue(command.targetRef, command.value)
|
||||
e.finished = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// concatCommand
|
||||
func newConcatCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("concatCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("concatCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
valuesJson := json.Get("values")
|
||||
if !valuesJson.Exists() || !valuesJson.IsArray() {
|
||||
return nil, errors.New("concatCommand: values field is required and must be an array")
|
||||
}
|
||||
|
||||
values := make([]interface{}, 0, len(valuesJson.Array()))
|
||||
for _, item := range valuesJson.Array() {
|
||||
var value interface{}
|
||||
if item.IsObject() {
|
||||
valueRef, err := NewRef(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("concatCommand: failed to create ref from values field: %v\n %v", err, item.Raw)
|
||||
}
|
||||
if valueRef.GetStage() > targetRef.GetStage() {
|
||||
return nil, fmt.Errorf("concatCommand: the processing stage of value [%s] cannot be after the stage of target [%s]", Stage2String[valueRef.GetStage()], Stage2String[targetRef.GetStage()])
|
||||
}
|
||||
value = valueRef
|
||||
} else {
|
||||
value = item.String()
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return &concatCommand{
|
||||
targetRef: targetRef,
|
||||
values: values,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type concatCommand struct {
|
||||
targetRef *Ref
|
||||
values []interface{}
|
||||
}
|
||||
|
||||
func (c *concatCommand) GetType() string {
|
||||
return commandTypeConcat
|
||||
}
|
||||
|
||||
func (c *concatCommand) GetRefs() []*Ref {
|
||||
refs := []*Ref{c.targetRef}
|
||||
if c.values != nil && len(c.values) != 0 {
|
||||
for _, value := range c.values {
|
||||
if ref, ok := value.(*Ref); ok {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func (c *concatCommand) CreateExecutor() Executor {
|
||||
return &concatExecutor{command: c}
|
||||
}
|
||||
|
||||
type concatExecutor struct {
|
||||
baseExecutor
|
||||
command *concatCommand
|
||||
values []string
|
||||
}
|
||||
|
||||
func (e *concatExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *concatExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
|
||||
if e.values == nil {
|
||||
e.values = make([]string, len(command.values))
|
||||
}
|
||||
|
||||
for i, value := range command.values {
|
||||
if value == nil || e.values[i] != "" {
|
||||
continue
|
||||
}
|
||||
v := ""
|
||||
if s, ok := value.(string); ok {
|
||||
v = s
|
||||
} else if ref, ok := value.(*Ref); ok && ref.GetStage() == stage {
|
||||
v = editorContext.GetRefValue(ref)
|
||||
}
|
||||
e.values[i] = v
|
||||
}
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
result := ""
|
||||
for _, v := range e.values {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
result += v
|
||||
}
|
||||
log.Debugf("concatCommand: set %s to %s", command.targetRef, result)
|
||||
editorContext.SetRefValue(command.targetRef, result)
|
||||
e.finished = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyCommand
|
||||
func newCopyCommand(json gjson.Result) (Command, error) {
|
||||
var sourceRef *Ref
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("source"); !t.Exists() {
|
||||
return nil, errors.New("copyCommand: source field is required")
|
||||
} else {
|
||||
sourceRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copyCommand: failed to create ref from source field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("copyCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copyCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
if sourceRef.GetStage() > targetRef.GetStage() {
|
||||
return nil, fmt.Errorf("copyCommand: the processing stage of source [%s] cannot be after the stage of target [%s]", Stage2String[sourceRef.GetStage()], Stage2String[targetRef.GetStage()])
|
||||
}
|
||||
return ©Command{
|
||||
sourceRef: sourceRef,
|
||||
targetRef: targetRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type copyCommand struct {
|
||||
sourceRef *Ref
|
||||
targetRef *Ref
|
||||
}
|
||||
|
||||
func (c *copyCommand) GetType() string {
|
||||
return commandTypeCopy
|
||||
}
|
||||
|
||||
func (c *copyCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.sourceRef, c.targetRef}
|
||||
}
|
||||
|
||||
func (c *copyCommand) CreateExecutor() Executor {
|
||||
return ©Executor{command: c}
|
||||
}
|
||||
|
||||
type copyExecutor struct {
|
||||
baseExecutor
|
||||
command *copyCommand
|
||||
valueToCopy string
|
||||
}
|
||||
|
||||
func (e *copyExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *copyExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
|
||||
if command.sourceRef.GetStage() == stage {
|
||||
e.valueToCopy = editorContext.GetRefValue(command.sourceRef)
|
||||
log.Debugf("copyCommand: valueToCopy=%s", e.valueToCopy)
|
||||
}
|
||||
|
||||
if e.valueToCopy == "" {
|
||||
log.Debug("copyCommand: valueToCopy is empty. skip.")
|
||||
e.finished = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
editorContext.SetRefValue(command.targetRef, e.valueToCopy)
|
||||
log.Debugf("copyCommand: set %s to %s", e.valueToCopy, command.targetRef)
|
||||
e.finished = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteCommand
|
||||
func newDeleteCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("deleteCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deleteCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
return &deleteCommand{
|
||||
targetRef: targetRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type deleteCommand struct {
|
||||
targetRef *Ref
|
||||
}
|
||||
|
||||
func (c *deleteCommand) GetType() string {
|
||||
return commandTypeDelete
|
||||
}
|
||||
|
||||
func (c *deleteCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *deleteCommand) CreateExecutor() Executor {
|
||||
return &deleteExecutor{command: c}
|
||||
}
|
||||
|
||||
type deleteExecutor struct {
|
||||
baseExecutor
|
||||
command *deleteCommand
|
||||
}
|
||||
|
||||
func (e *deleteExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *deleteExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("deleteCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
log.Debugf("deleteCommand: delete %s", command.targetRef)
|
||||
editorContext.DeleteRefValues(command.targetRef)
|
||||
e.finished = true
|
||||
log.Debugf("deleteCommand: finished deleting %s", command.targetRef)
|
||||
} else {
|
||||
log.Debugf("deleteCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renameCommand
|
||||
func newRenameCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("renameCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("renameCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
newName := json.Get("newName").String()
|
||||
if newName == "" {
|
||||
return nil, errors.New("renameCommand: newName field is required")
|
||||
}
|
||||
return &renameCommand{
|
||||
targetRef: targetRef,
|
||||
newName: newName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type renameCommand struct {
|
||||
targetRef *Ref
|
||||
newName string
|
||||
}
|
||||
|
||||
func (c *renameCommand) GetType() string {
|
||||
return commandTypeRename
|
||||
}
|
||||
|
||||
func (c *renameCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *renameCommand) CreateExecutor() Executor {
|
||||
return &renameExecutor{command: c}
|
||||
}
|
||||
|
||||
type renameExecutor struct {
|
||||
baseExecutor
|
||||
command *renameCommand
|
||||
}
|
||||
|
||||
func (e *renameExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *renameExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("renameCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
if command.newName == command.targetRef.Name {
|
||||
log.Debugf("renameCommand: skip renaming %s to itself", command.targetRef)
|
||||
} else {
|
||||
values := editorContext.GetRefValues(command.targetRef)
|
||||
log.Debugf("renameCommand: rename %s to %s value=%v", command.targetRef, command.newName, values)
|
||||
editorContext.SetRefValues(&Ref{
|
||||
Type: command.targetRef.Type,
|
||||
Name: command.newName,
|
||||
}, values)
|
||||
editorContext.DeleteRefValues(command.targetRef)
|
||||
log.Debugf("renameCommand: finished renaming %s to %s", command.targetRef, command.newName)
|
||||
}
|
||||
e.finished = true
|
||||
} else {
|
||||
log.Debugf("renameCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
309
plugins/wasm-go/extensions/traffic-editor/pkg/command_test.go
Normal file
309
plugins/wasm-go/extensions/traffic-editor/pkg/command_test.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestNewSetCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"},"value":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newSetCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "set" {
|
||||
t.Errorf("expected type 'set', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSetCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"set","value":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newSetCommand(json)
|
||||
if err == nil || err.Error() != "setCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSetCommand_MissingValue(t *testing.T) {
|
||||
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newSetCommand(json)
|
||||
if err == nil || err.Error() != "setCommand: value field is required" {
|
||||
t.Errorf("expected value field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"},"values":["a","b"]}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newConcatCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "concat" {
|
||||
t.Errorf("expected type 'concat', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) < 1 {
|
||||
t.Errorf("expected at least 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","values":["a","b"]}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newConcatCommand(json)
|
||||
if err == nil || err.Error() != "concatCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_MissingValues(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newConcatCommand(json)
|
||||
if err == nil || err.Error() != "concatCommand: values field is required and must be an array" {
|
||||
t.Errorf("expected values field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newCopyCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "copy" {
|
||||
t.Errorf("expected type 'copy', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 2 {
|
||||
t.Errorf("expected 2 refs, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_MissingSource(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: source field is required" {
|
||||
t.Errorf("expected source field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_SourceStageAfterTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"response_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: the processing stage of source [response_headers] cannot be after the stage of target [request_headers]" {
|
||||
t.Errorf("expected source stage field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDeleteCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"delete","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newDeleteCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "delete" {
|
||||
t.Errorf("expected type 'delete', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDeleteCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"delete"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newDeleteCommand(json)
|
||||
if err == nil || err.Error() != "deleteCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"},"newName":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newRenameCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "rename" {
|
||||
t.Errorf("expected type 'rename', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","newName":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newRenameCommand(json)
|
||||
if err == nil || err.Error() != "renameCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_MissingNewName(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newRenameCommand(json)
|
||||
if err == nil || err.Error() != "renameCommand: newName field is required" {
|
||||
t.Errorf("expected newName field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExecutor_Run_SingleStage(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
cmd := &setCommand{targetRef: ref, value: "bar"}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "bar" {
|
||||
t.Errorf("expected value 'bar', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcatExecutor_Run_SingleStage(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
|
||||
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(srcRef, "-")
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "a-b" {
|
||||
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcatExecutor_Run_MultiStages(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeResponseHeader, Name: "foo"}
|
||||
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
|
||||
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(srcRef, "-")
|
||||
|
||||
err := executor.Run(ctx, StageRequestHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
err = executor.Run(ctx, StageResponseHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "a-b" {
|
||||
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyExecutor_Run_SingleStage(t *testing.T) {
|
||||
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
target := &Ref{Type: RefTypeRequestHeader, Name: "bar"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(source, "baz")
|
||||
cmd := ©Command{sourceRef: source, targetRef: target}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(target) != "baz" {
|
||||
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyExecutor_Run_MultiStages(t *testing.T) {
|
||||
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
target := &Ref{Type: RefTypeResponseHeader, Name: "bar"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(source, "baz")
|
||||
cmd := ©Command{sourceRef: source, targetRef: target}
|
||||
executor := cmd.CreateExecutor()
|
||||
|
||||
err := executor.Run(ctx, StageRequestHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
err = executor.Run(ctx, StageResponseHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(target) != "baz" {
|
||||
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteExecutor_Run(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(ref, "bar")
|
||||
cmd := &deleteCommand{targetRef: ref}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "" {
|
||||
t.Errorf("expected value to be deleted, got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameExecutor_Run(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(ref, "bar")
|
||||
cmd := &renameCommand{targetRef: ref, newName: "baz"}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
newRef := &Ref{Type: ref.Type, Name: "baz"}
|
||||
if ctx.GetRefValue(newRef) != "bar" {
|
||||
t.Errorf("expected value 'bar' for new name, got %s", ctx.GetRefValue(newRef))
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "" {
|
||||
t.Errorf("expected old name to be deleted, got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
325
plugins/wasm-go/extensions/traffic-editor/pkg/condition.go
Normal file
325
plugins/wasm-go/extensions/traffic-editor/pkg/condition.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
conditionTypeEquals = "equals"
|
||||
conditionTypePrefix = "prefix"
|
||||
conditionTypeSuffix = "suffix"
|
||||
conditionTypeContains = "contains"
|
||||
conditionTypeRegex = "regex"
|
||||
)
|
||||
|
||||
var (
|
||||
conditionFactories = map[string]func(gjson.Result) (Condition, error){
|
||||
conditionTypeEquals: newEqualsCondition,
|
||||
conditionTypePrefix: newPrefixCondition,
|
||||
conditionTypeSuffix: newSuffixCondition,
|
||||
conditionTypeContains: newContainsCondition,
|
||||
conditionTypeRegex: newRegexCondition,
|
||||
}
|
||||
)
|
||||
|
||||
type ConditionSet struct {
|
||||
Conditions []Condition `json:"conditions,omitempty"`
|
||||
RelatedStages map[Stage]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *ConditionSet) FromJson(json gjson.Result) error {
|
||||
relatedStages := map[Stage]bool{}
|
||||
s.Conditions = nil
|
||||
if conditionsJson := json.Get("conditions"); conditionsJson.Exists() && conditionsJson.IsArray() {
|
||||
for _, item := range conditionsJson.Array() {
|
||||
if condition, err := CreateCondition(item); err != nil {
|
||||
return fmt.Errorf("failed to create condition from json: %v\n %v", err, item)
|
||||
} else {
|
||||
s.Conditions = append(s.Conditions, condition)
|
||||
for _, ref := range condition.GetRefs() {
|
||||
relatedStages[ref.GetStage()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.RelatedStages = relatedStages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConditionSet) Matches(editorContext EditorContext) bool {
|
||||
if len(s.Conditions) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, condition := range s.Conditions {
|
||||
if !condition.Evaluate(editorContext) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Condition interface {
|
||||
GetType() string
|
||||
GetRefs() []*Ref
|
||||
Evaluate(ctx EditorContext) bool
|
||||
}
|
||||
|
||||
func CreateCondition(json gjson.Result) (Condition, error) {
|
||||
t := json.Get("type").String()
|
||||
if t == "" {
|
||||
return nil, errors.New("condition type is required")
|
||||
}
|
||||
if constructor, ok := conditionFactories[t]; !ok || constructor == nil {
|
||||
return nil, errors.New("unknown condition type: " + t)
|
||||
} else if condition, err := constructor(json); err != nil {
|
||||
return nil, fmt.Errorf("failed to create condition with type %s: %v", t, err)
|
||||
} else {
|
||||
for _, ref := range condition.GetRefs() {
|
||||
if ref.GetStage() >= StageResponseHeaders {
|
||||
return nil, fmt.Errorf("condition only supports request refs")
|
||||
}
|
||||
}
|
||||
return condition, nil
|
||||
}
|
||||
}
|
||||
|
||||
// equalsCondition
|
||||
func newEqualsCondition(json gjson.Result) (Condition, error) {
|
||||
value1 := json.Get("value1")
|
||||
if value1.Type != gjson.JSON {
|
||||
return nil, errors.New("equalsCondition: value1 field type must be JSON object")
|
||||
}
|
||||
value1Ref, err := NewRef(value1)
|
||||
if err != nil {
|
||||
return nil, errors.New("equalsCondition: failed to create value1 ref: " + err.Error())
|
||||
}
|
||||
value2 := json.Get("value2").String()
|
||||
return &equalsCondition{
|
||||
value1Ref: value1Ref,
|
||||
value2: value2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type equalsCondition struct {
|
||||
value1Ref *Ref
|
||||
value2 string
|
||||
}
|
||||
|
||||
func (c *equalsCondition) GetType() string {
|
||||
return conditionTypeEquals
|
||||
}
|
||||
|
||||
func (c *equalsCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.value1Ref}
|
||||
}
|
||||
|
||||
func (c *equalsCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating equals condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
|
||||
ref1Values := ctx.GetRefValues(c.value1Ref)
|
||||
if len(ref1Values) == 0 {
|
||||
log.Debugf("No values found for ref1: %v", c.value1Ref)
|
||||
return false
|
||||
}
|
||||
for _, value1 := range ref1Values {
|
||||
if value1 == c.value2 {
|
||||
log.Debugf("Condition matched: %s == %s", value1, c.value2)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
|
||||
return false
|
||||
}
|
||||
|
||||
// prefixCondition
|
||||
func newPrefixCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("prefixCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("prefixCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
prefix := json.Get("prefix").String()
|
||||
return &prefixCondition{
|
||||
valueRef: valueRef,
|
||||
prefix: prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type prefixCondition struct {
|
||||
valueRef *Ref
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *prefixCondition) GetType() string {
|
||||
return conditionTypePrefix
|
||||
}
|
||||
|
||||
func (c *prefixCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
|
||||
func (c *prefixCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating prefix condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.HasPrefix(value, c.prefix) {
|
||||
log.Debugf("Condition matched: %s starts with %s", value, c.prefix)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
|
||||
return false
|
||||
}
|
||||
|
||||
// suffixCondition
|
||||
func newSuffixCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("suffixCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("suffixCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
suffix := json.Get("suffix").String()
|
||||
return &suffixCondition{
|
||||
valueRef: valueRef,
|
||||
suffix: suffix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type suffixCondition struct {
|
||||
valueRef *Ref
|
||||
suffix string
|
||||
}
|
||||
|
||||
func (c *suffixCondition) GetType() string {
|
||||
return conditionTypeSuffix
|
||||
}
|
||||
|
||||
func (c *suffixCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
func (c *suffixCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating suffix condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.HasSuffix(value, c.suffix) {
|
||||
log.Debugf("Condition matched: %s ends with %s", value, c.suffix)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
|
||||
return false
|
||||
}
|
||||
|
||||
// containsCondition
|
||||
func newContainsCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("containsCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("containsCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
part := json.Get("part").String()
|
||||
return &containsCondition{
|
||||
valueRef: valueRef,
|
||||
part: part,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type containsCondition struct {
|
||||
valueRef *Ref
|
||||
part string
|
||||
}
|
||||
|
||||
func (c *containsCondition) GetType() string {
|
||||
return conditionTypeContains
|
||||
}
|
||||
|
||||
func (c *containsCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
|
||||
func (c *containsCondition) Evaluate(ctx EditorContext) bool {
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.Contains(value, c.part) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// regexCondition
|
||||
func newRegexCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("regexCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("regexCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
patternStr := json.Get("pattern").String()
|
||||
pattern, err := regexp.Compile(patternStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("regexCondition: failed to compile pattern: " + err.Error())
|
||||
}
|
||||
return ®exCondition{
|
||||
valueRef: valueRef,
|
||||
pattern: pattern,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type regexCondition struct {
|
||||
valueRef *Ref
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
func (c *regexCondition) GetType() string {
|
||||
return conditionTypeRegex
|
||||
}
|
||||
|
||||
func (c *regexCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating regex condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if c.pattern.MatchString(value) {
|
||||
log.Debugf("Condition matched: %s matches %s", value, c.pattern.String())
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *regexCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
217
plugins/wasm-go/extensions/traffic-editor/pkg/condition_test.go
Normal file
217
plugins/wasm-go/extensions/traffic-editor/pkg/condition_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// --- equalsCondition tests ---
|
||||
func TestEqualsCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("equalsCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualsCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"def"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("equalsCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- prefixCondition tests ---
|
||||
func TestPrefixCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("prefixCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bazbar"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("prefixCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- suffixCondition tests ---
|
||||
func TestSuffixCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-end": {"123xyz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("suffixCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-end": {"xyz123"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("suffixCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- containsCondition tests ---
|
||||
func TestContainsCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("containsCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("containsCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- regexCondition tests ---
|
||||
func TestRegexCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"abcdef"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("regexCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"defabc"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("regexCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- CreateCondition error cases ---
|
||||
func TestCreateCondition_UnknownType(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"unknown","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_MissingType(t *testing.T) {
|
||||
json := gjson.Parse(`{"value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for missing type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_InvalidRefType(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"invalid_type","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for invalid ref type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_MissingRefName(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for missing ref name")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ConditionSet tests ---
|
||||
func TestConditionSet_Matches_AllMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !set.Matches(ctx) {
|
||||
t.Error("ConditionSet should match when all conditions match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionSet_Matches_OneNoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"baz"}})
|
||||
if set.Matches(ctx) {
|
||||
t.Error("ConditionSet should not match if one condition does not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionSet_Matches_Empty(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
if !set.Matches(ctx) {
|
||||
t.Error("ConditionSet with no conditions should always match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetType/GetRefs coverage ---
|
||||
func TestCondition_GetTypeAndRefs(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
if cond.GetType() != "equals" {
|
||||
t.Error("GetType should return 'equals'")
|
||||
}
|
||||
refs := cond.GetRefs()
|
||||
if len(refs) != 1 || refs[0].Type != "request_header" || refs[0].Name != "x-test" {
|
||||
t.Error("GetRefs should return correct ref")
|
||||
}
|
||||
}
|
||||
310
plugins/wasm-go/extensions/traffic-editor/pkg/context.go
Normal file
310
plugins/wasm-go/extensions/traffic-editor/pkg/context.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
type Stage int
|
||||
|
||||
const (
|
||||
StageInvalid Stage = iota
|
||||
StageRequestHeaders
|
||||
StageRequestBody
|
||||
StageResponseHeaders
|
||||
StageResponseBody
|
||||
|
||||
pathHeader = ":path"
|
||||
)
|
||||
|
||||
var (
|
||||
OrderedStages = []Stage{
|
||||
StageRequestHeaders,
|
||||
StageRequestBody,
|
||||
StageResponseHeaders,
|
||||
StageResponseBody,
|
||||
}
|
||||
Stage2String = map[Stage]string{
|
||||
StageRequestHeaders: "request_headers",
|
||||
StageRequestBody: "request_body",
|
||||
StageResponseHeaders: "response_headers",
|
||||
StageResponseBody: "response_body",
|
||||
}
|
||||
)
|
||||
|
||||
type EditorContext interface {
|
||||
GetEffectiveCommandSet() *CommandSet
|
||||
SetEffectiveCommandSet(cmdSet *CommandSet)
|
||||
GetCommandExecutors() []Executor
|
||||
SetCommandExecutors(executors []Executor)
|
||||
GetCurrentStage() Stage
|
||||
SetCurrentStage(stage Stage)
|
||||
|
||||
GetRequestPath() string
|
||||
SetRequestPath(path string)
|
||||
GetRequestHeader(key string) []string
|
||||
GetRequestHeaders() map[string][]string
|
||||
SetRequestHeaders(map[string][]string)
|
||||
GetRequestQuery(key string) []string
|
||||
GetRequestQueries() map[string][]string
|
||||
SetRequestQueries(map[string][]string)
|
||||
GetResponseHeader(key string) []string
|
||||
GetResponseHeaders() map[string][]string
|
||||
SetResponseHeaders(map[string][]string)
|
||||
|
||||
GetRefValue(ref *Ref) string
|
||||
GetRefValues(ref *Ref) []string
|
||||
SetRefValue(ref *Ref, value string)
|
||||
SetRefValues(ref *Ref, values []string)
|
||||
DeleteRefValues(ref *Ref)
|
||||
|
||||
IsRequestHeadersDirty() bool
|
||||
IsResponseHeadersDirty() bool
|
||||
ResetDirtyFlags()
|
||||
}
|
||||
|
||||
func NewEditorContext() EditorContext {
|
||||
return &editorContext{}
|
||||
}
|
||||
|
||||
type editorContext struct {
|
||||
effectiveCommandSet *CommandSet
|
||||
commandExecutors []Executor
|
||||
|
||||
currentStage Stage
|
||||
|
||||
requestPath string
|
||||
requestHeaders map[string][]string
|
||||
requestQueries map[string][]string
|
||||
responseHeaders map[string][]string
|
||||
|
||||
requestHeadersDirty bool
|
||||
responseHeadersDirty bool
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetEffectiveCommandSet() *CommandSet {
|
||||
return ctx.effectiveCommandSet
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetEffectiveCommandSet(cmdSet *CommandSet) {
|
||||
ctx.effectiveCommandSet = cmdSet
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetCommandExecutors() []Executor {
|
||||
return ctx.commandExecutors
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetCommandExecutors(executors []Executor) {
|
||||
ctx.commandExecutors = executors
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetCurrentStage() Stage {
|
||||
return ctx.currentStage
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetCurrentStage(stage Stage) {
|
||||
ctx.currentStage = stage
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestPath() string {
|
||||
return ctx.requestPath
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestPath(path string) {
|
||||
ctx.requestPath = path
|
||||
ctx.savePathToHeader()
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestHeader(key string) []string {
|
||||
if ctx.requestHeaders == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.requestHeaders[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestHeaders() map[string][]string {
|
||||
return maps.Clone(ctx.requestHeaders)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestHeaders(headers map[string][]string) {
|
||||
ctx.requestHeaders = headers
|
||||
ctx.loadPathFromHeader()
|
||||
ctx.requestHeadersDirty = true
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestQuery(key string) []string {
|
||||
if ctx.requestQueries == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.requestQueries[key]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestQueries() map[string][]string {
|
||||
return maps.Clone(ctx.requestQueries)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestQueries(queries map[string][]string) {
|
||||
ctx.requestQueries = queries
|
||||
ctx.savePathToHeader()
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetResponseHeader(key string) []string {
|
||||
if ctx.responseHeaders == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.responseHeaders[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetResponseHeaders() map[string][]string {
|
||||
return maps.Clone(ctx.responseHeaders)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetResponseHeaders(headers map[string][]string) {
|
||||
ctx.responseHeaders = headers
|
||||
ctx.responseHeadersDirty = true
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRefValue(ref *Ref) string {
|
||||
values := ctx.GetRefValues(ref)
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRefValues(ref *Ref) []string {
|
||||
if ref == nil {
|
||||
return nil
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
return ctx.GetRequestHeader(strings.ToLower(ref.Name))
|
||||
case RefTypeRequestQuery:
|
||||
return ctx.GetRequestQuery(ref.Name)
|
||||
case RefTypeResponseHeader:
|
||||
return ctx.GetResponseHeader(strings.ToLower(ref.Name))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRefValue(ref *Ref, value string) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
ctx.SetRefValues(ref, []string{value})
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRefValues(ref *Ref, values []string) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
if ctx.requestHeaders == nil {
|
||||
ctx.requestHeaders = make(map[string][]string)
|
||||
}
|
||||
loweredRefName := strings.ToLower(ref.Name)
|
||||
ctx.requestHeaders[loweredRefName] = values
|
||||
ctx.requestHeadersDirty = true
|
||||
if loweredRefName == pathHeader {
|
||||
ctx.loadPathFromHeader()
|
||||
}
|
||||
break
|
||||
case RefTypeRequestQuery:
|
||||
if ctx.requestQueries == nil {
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
}
|
||||
ctx.requestQueries[ref.Name] = values
|
||||
ctx.savePathToHeader()
|
||||
break
|
||||
case RefTypeResponseHeader:
|
||||
if ctx.responseHeaders == nil {
|
||||
ctx.responseHeaders = make(map[string][]string)
|
||||
}
|
||||
ctx.responseHeaders[strings.ToLower(ref.Name)] = values
|
||||
ctx.responseHeadersDirty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) DeleteRefValues(ref *Ref) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
delete(ctx.requestHeaders, strings.ToLower(ref.Name))
|
||||
ctx.requestHeadersDirty = true
|
||||
break
|
||||
case RefTypeRequestQuery:
|
||||
delete(ctx.requestQueries, ref.Name)
|
||||
ctx.savePathToHeader()
|
||||
break
|
||||
case RefTypeResponseHeader:
|
||||
delete(ctx.responseHeaders, strings.ToLower(ref.Name))
|
||||
ctx.responseHeadersDirty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) IsRequestHeadersDirty() bool {
|
||||
return ctx.requestHeadersDirty
|
||||
}
|
||||
|
||||
func (ctx *editorContext) IsResponseHeadersDirty() bool {
|
||||
return ctx.responseHeadersDirty
|
||||
}
|
||||
|
||||
func (ctx *editorContext) ResetDirtyFlags() {
|
||||
ctx.requestHeadersDirty = false
|
||||
ctx.responseHeadersDirty = false
|
||||
}
|
||||
|
||||
func (ctx *editorContext) savePathToHeader() {
|
||||
u, err := url.Parse(ctx.requestPath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to build the new path with query strings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
for k, vs := range ctx.requestQueries {
|
||||
for _, v := range vs {
|
||||
query.Add(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
ctx.SetRefValue(&Ref{Type: RefTypeRequestHeader, Name: pathHeader}, u.String())
|
||||
}
|
||||
|
||||
func (ctx *editorContext) loadPathFromHeader() {
|
||||
paths := ctx.GetRequestHeader(pathHeader)
|
||||
|
||||
if len(paths) == 0 || paths[0] == "" {
|
||||
log.Warn("the request has an empty path")
|
||||
ctx.requestPath = ""
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
return
|
||||
}
|
||||
|
||||
path := paths[0]
|
||||
queries := make(map[string][]string)
|
||||
|
||||
u, err := url.Parse(path)
|
||||
if err != nil {
|
||||
log.Warnf("unable to parse the request path: %s", path)
|
||||
ctx.requestPath = ""
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.requestPath = u.Path
|
||||
for k, vs := range u.Query() {
|
||||
queries[k] = vs
|
||||
}
|
||||
ctx.requestQueries = queries
|
||||
}
|
||||
218
plugins/wasm-go/extensions/traffic-editor/pkg/context_test.go
Normal file
218
plugins/wasm-go/extensions/traffic-editor/pkg/context_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newTestRef(t, name string) *Ref {
|
||||
return &Ref{Type: t, Name: name}
|
||||
}
|
||||
|
||||
func TestEditorContext_CommandSetAndExecutors(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
cmdSet := &CommandSet{}
|
||||
ctx.SetEffectiveCommandSet(cmdSet)
|
||||
if ctx.GetEffectiveCommandSet() != cmdSet {
|
||||
t.Errorf("EffectiveCommandSet not set/get correctly")
|
||||
}
|
||||
|
||||
executors := []Executor{nil, nil}
|
||||
ctx.SetCommandExecutors(executors)
|
||||
if !reflect.DeepEqual(ctx.GetCommandExecutors(), executors) {
|
||||
t.Errorf("CommandExecutors not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_Stage(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetCurrentStage(StageRequestHeaders)
|
||||
if ctx.GetCurrentStage() != StageRequestHeaders {
|
||||
t.Errorf("CurrentStage not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestPath(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetRequestPath("/foo/bar")
|
||||
if ctx.GetRequestPath() != "/foo/bar" {
|
||||
t.Errorf("RequestPath not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetRequestHeaders(headers)
|
||||
if !reflect.DeepEqual(ctx.GetRequestHeaders(), headers) {
|
||||
t.Errorf("RequestHeaders not set/get correctly")
|
||||
}
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetRequestHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetRequestHeader failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestQueries(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
queries := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetRequestQueries(queries)
|
||||
if !reflect.DeepEqual(ctx.GetRequestQueries(), queries) {
|
||||
t.Errorf("RequestQueries not set/get correctly")
|
||||
}
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetRequestQuery("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetRequestQuery failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_ResponseHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetResponseHeaders(headers)
|
||||
if !reflect.DeepEqual(ctx.GetResponseHeaders(), headers) {
|
||||
t.Errorf("ResponseHeaders not set/get correctly")
|
||||
}
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetResponseHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetResponseHeader failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RefValueAndValues(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
rh := newTestRef(RefTypeRequestHeader, "foo")
|
||||
rq := newTestRef(RefTypeRequestQuery, "bar")
|
||||
rh2 := newTestRef(RefTypeResponseHeader, "baz")
|
||||
|
||||
ctx.SetRefValue(rh, "v1")
|
||||
ctx.SetRefValues(rq, []string{"v2", "v3"})
|
||||
ctx.SetRefValues(rh2, []string{"v4"})
|
||||
|
||||
if v := ctx.GetRefValue(rh); v != "v1" {
|
||||
t.Errorf("GetRefValue(RequestHeader) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rq); !reflect.DeepEqual(v, []string{"v2", "v3"}) {
|
||||
t.Errorf("GetRefValues(RequestQuery) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rh2); !reflect.DeepEqual(v, []string{"v4"}) {
|
||||
t.Errorf("GetRefValues(ResponseHeader) failed: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_DeleteRefValues(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
rh := newTestRef(RefTypeRequestHeader, "foo")
|
||||
rq := newTestRef(RefTypeRequestQuery, "bar")
|
||||
rh2 := newTestRef(RefTypeResponseHeader, "baz")
|
||||
|
||||
ctx.SetRefValue(rh, "v1")
|
||||
ctx.SetRefValues(rq, []string{"v2", "v3"})
|
||||
ctx.SetRefValues(rh2, []string{"v4"})
|
||||
|
||||
ctx.DeleteRefValues(rh)
|
||||
ctx.DeleteRefValues(rq)
|
||||
ctx.DeleteRefValues(rh2)
|
||||
|
||||
if v := ctx.GetRefValues(rh); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(RequestHeader) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rq); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(RequestQuery) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rh2); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(ResponseHeader) failed: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_ResetDirtyFlags(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() || ctx.IsRequestHeadersDirty() || ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResetDirtyFlags failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsRequestHeadersDirty_SetHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be false initially")
|
||||
}
|
||||
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after SetRequestHeaders")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeRequestHeader, "foo")
|
||||
ctx.SetRefValue(ref, "baz")
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after SetRefValue")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsRequestHeadersDirty_SetQueries(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be false initially")
|
||||
}
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after SetRequestQueries")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeRequestQuery, "foo")
|
||||
ctx.SetRefValues(ref, []string{"baz"})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after SetRefValues")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsResponseHeadersDirty(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be false initially")
|
||||
}
|
||||
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after SetResponseHeaders")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeResponseHeader, "foo")
|
||||
ctx.SetRefValues(ref, []string{"baz"})
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after SetRefValues")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
26
plugins/wasm-go/extensions/traffic-editor/pkg/mock_test.go
Normal file
26
plugins/wasm-go/extensions/traffic-editor/pkg/mock_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize mock logger for testing
|
||||
log.SetPluginLog(&mockLogger{})
|
||||
}
|
||||
|
||||
type mockLogger struct{}
|
||||
|
||||
func (m *mockLogger) Trace(msg string) {}
|
||||
func (m *mockLogger) Tracef(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Debug(msg string) {}
|
||||
func (m *mockLogger) Debugf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Info(msg string) {}
|
||||
func (m *mockLogger) Infof(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Warn(msg string) {}
|
||||
func (m *mockLogger) Warnf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Error(msg string) {}
|
||||
func (m *mockLogger) Errorf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Critical(msg string) {}
|
||||
func (m *mockLogger) Criticalf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) ResetID(pluginID string) {}
|
||||
64
plugins/wasm-go/extensions/traffic-editor/pkg/ref.go
Normal file
64
plugins/wasm-go/extensions/traffic-editor/pkg/ref.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
RefTypeRequestHeader = "request_header"
|
||||
RefTypeRequestQuery = "request_query"
|
||||
RefTypeResponseHeader = "response_header"
|
||||
)
|
||||
|
||||
var (
|
||||
refType2Stage = map[string]Stage{
|
||||
RefTypeRequestHeader: StageRequestHeaders,
|
||||
RefTypeRequestQuery: StageRequestHeaders,
|
||||
RefTypeResponseHeader: StageResponseHeaders,
|
||||
}
|
||||
)
|
||||
|
||||
type Ref struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
stage Stage
|
||||
}
|
||||
|
||||
func NewRef(json gjson.Result) (*Ref, error) {
|
||||
ref := &Ref{}
|
||||
|
||||
if t := json.Get("type").String(); t != "" {
|
||||
ref.Type = t
|
||||
} else {
|
||||
return nil, errors.New("missing type field")
|
||||
}
|
||||
|
||||
if _, ok := refType2Stage[ref.Type]; !ok {
|
||||
return nil, fmt.Errorf("unknown ref type: %s", ref.Type)
|
||||
}
|
||||
|
||||
if name := json.Get("name").String(); name != "" {
|
||||
ref.Name = name
|
||||
} else {
|
||||
return nil, errors.New("missing name field")
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func (r *Ref) GetStage() Stage {
|
||||
if r.stage == 0 {
|
||||
if stage, ok := refType2Stage[r.Type]; ok {
|
||||
r.stage = stage
|
||||
}
|
||||
}
|
||||
return r.stage
|
||||
}
|
||||
|
||||
func (r *Ref) String() string {
|
||||
return fmt.Sprintf("%s/%s", r.Type, r.Name)
|
||||
}
|
||||
Reference in New Issue
Block a user