Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions plugins/wasm-go/extensions/traffic-tag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ description: 流量染色插件配置参考

`conditions` 中每一项的配置字段说明如下:

| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|----------------|--------|-------|---------|-------------------------------------------------------------|
| `conditionType`| string | - | 是 | 条件类型,支持 `header`、`parameter`、`cookie`。 |
| `key` | string | - | 是 | 条件的关键字。 |
| `operator` | string | - | 是 | 操作符,支持 `equal`、`not_equal`、`prefix`、`in`、`not_in`、`regex`、`percentage` |
| `value` | array of string | - | 是 | 条件的值,**仅当**操作符为 `in` 和 `not_in` 时支持配置多个值。 |
| 字段名称 | 类型 | 默认值 | 是否必填 | 描述 |
|----------------|--------|-------|---------|-----------------------------------------------------------------------------------------------|
| `conditionType`| string | - | 是 | 条件类型,支持 `header`、`parameter`、`cookie`。 |
| `key` | string | - | 是 | 条件的关键字。 |
| `operator` | string | - | 是 | 操作符,支持 `equal`、`not_equal`、`prefix`、`in`、`not_in`、`regex`、`percentage`、`exists`、`not_exists`。 |
| `value` | array of string | - | 是 | 条件的值,**仅当**操作符为 `in` 和 `not_in` 时支持配置多个值。 |

> **说明:当 `operator` 为 `regex` 时,使用的正则表达式引擎是 [RE2](https://github.com/google/re2)。详情请参阅 [RE2 官方文档](https://github.com/google/re2/wiki/Syntax)。

Expand All @@ -59,15 +59,17 @@ description: 流量染色插件配置参考
| `weight` | integer | - | 是 | 流量权重百分比。 |

### 操作符说明
| 操作符 | 描述 |
|-------------|------------------------------------------|
| `equal` | 精确匹配,值需要完全相等 |
| `not_equal` | 不等匹配,值不相等时满足条件 |
| `prefix` | 前缀匹配,指定值是实际值的前缀时满足条件 |
| `in` | 包含匹配,实际值需要在指定的列表中 |
| `not_in` | 排除匹配,实际值不在指定的列表中时满足条件|
| `regex` | 正则表达式匹配,按照正则表达式规则匹配 |
| `percentage`| 百分比匹配,原理:`hash(get(key)) % 100 < value` 成立时满足条件|
| 操作符 | 描述 |
|--------------|--------------------------------------------------|
| `equal` | 精确匹配,值需要完全相等 |
| `not_equal` | 不等匹配,值不相等时满足条件 |
| `prefix` | 前缀匹配,指定值是实际值的前缀时满足条件 |
| `in` | 包含匹配,实际值需要在指定的列表中 |
| `not_in` | 排除匹配,实际值不在指定的列表中时满足条件 |
| `regex` | 正则表达式匹配,按照正则表达式规则匹配 |
| `percentage` | 百分比匹配,原理:`hash(get(key)) % 100 < value` 成立时满足条件 |
| `exists` | 存在性匹配,指定的key存在时满足条件, 不需要传递value |
| `not_exists` | 存在性匹配,指定的key不存在时满足条件, 不需要传递value |

> **提示:关于`percentage`和`weight`的区别**
>
Expand Down
30 changes: 16 additions & 14 deletions plugins/wasm-go/extensions/traffic-tag/README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ The configuration fields for each item in `conditionGroups` are described as fol
---
The configuration fields for each item in `conditions` are described as follows:

| Field Name | Type | Default Value | Required | Description |
|-------------------|-------------------|---------------|----------|-------------------------------------------------------------------|
| `conditionType` | string | - | Yes | Condition type, supports `header`, `parameter`, `cookie`. |
| `key` | string | - | Yes | The key of the condition. |
| `operator` | string | - | Yes | Operator, supports `equal`, `not_equal`, `prefix`, `in`, `not_in`, `regex`, `percentage`. |
| `value` | array of string | - | Yes | The value of the condition. **Only when** the operator is `in` and `not_in` multiple values are supported. |
| Field Name | Type | Default Value | Required | Description |
|-------------------|-------------------|---------------|----------|------------------------------------------------------------------------------------------------------------------|
| `conditionType` | string | - | Yes | Condition type, supports `header`, `parameter`, `cookie`. |
| `key` | string | - | Yes | The key of the condition. |
| `operator` | string | - | Yes | Operator, supports `equal`, `not_equal`, `prefix`, `in`, `not_in`, `regex`, `percentage`, `exists`, not_exists`. |
| `value` | array of string | - | Yes | The value of the condition. **Only when** the operator is `in` and `not_in` multiple values are supported. |

> **Note: When the `operator` is `regex`, the regular expression engine used is [RE2](https://github.com/google/re2). For details, please refer to the [RE2 Official Documentation](https://github.com/google/re2/wiki/Syntax).**

Expand All @@ -51,15 +51,17 @@ The configuration fields for each item in `weightGroups` are described as follow
| `weight` | integer | - | Yes | Traffic weight percentage. |

### Operator Description
| Operator | Description |
|---------------|----------------------------------------------------|
| `equal` | Exact match, values must be identical. |
| `not_equal` | Not equal match, condition met when values are different. |
| `prefix` | Prefix match, condition met when the specified value is a prefix of the actual value. |
| `in` | Inclusion match, actual value must be in the specified list. |
| `not_in` | Exclusion match, condition met when actual value is not in the specified list. |
| `regex` | Regular expression match, matched according to regex rules. |
| Operator | Description |
|----------------|---------------------------------------------------------------------------------------|
| `equal` | Exact match, values must be identical. |
| `not_equal` | Not equal match, condition met when values are different. |
| `prefix` | Prefix match, condition met when the specified value is a prefix of the actual value. |
| `in` | Inclusion match, actual value must be in the specified list. |
| `not_in` | Exclusion match, condition met when actual value is not in the specified list. |
| `regex` | Regular expression match, matched according to regex rules. |
| `percentage` | Percentage match, principle: `hash(get(key)) % 100 < value`, condition met when true. |
| `exists` | Exist match,condition met when values exists, no value needed |
| `not_exists` | Exist match,condition met when values not exists, no value needed |

> **Tip: About the difference between `percentage` and `weight`**
>
Expand Down
22 changes: 21 additions & 1 deletion plugins/wasm-go/extensions/traffic-tag/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ func onContentRequestHeaders(conditionGroups []ConditionGroup, log log.Log) bool
func matchCondition(conditionGroup *ConditionGroup, log log.Log) bool {
for _, condition := range conditionGroup.Conditions {
conditionKeyValue, err := getConditionValue(condition, log)
if err != nil {
// 判断值是否存在
isExist := err == nil

// 如果是 DoesNotExist,不能在这里因为 err != nil 就提前拦截掉,必须放行到 switch 中处理。
// 对于其他常规操作符(比如 =, prefix, regex 等),如果没取到值,依旧按照原有逻辑认为匹配失败。
if !isExist && condition.Operator != Op_NotExists {
log.Debugf("failed to get condition value: %s", err)
if conditionGroup.Logic == "and" {
return false
Expand All @@ -49,6 +54,21 @@ func matchCondition(conditionGroup *ConditionGroup, log log.Log) bool {
}

switch condition.Operator {
case Op_Exists:
// 如果走到这里,说明上面没有被 return/continue 拦截,
// 意味着 isExist 必定为 true!所以必定Match
if conditionGroup.Logic == "or" {
log.Debugf("condition match: exists")
return true
}
case Op_NotExists:
if !isExist && conditionGroup.Logic == "or" {
log.Debugf("condition match: not exist")
return true
} else if isExist && conditionGroup.Logic == "and" {
log.Debugf("condition not match: not exist")
return false
}
case Op_Equal:
if conditionKeyValue == condition.Value[0] && conditionGroup.Logic == "or" {
log.Debugf("condition match: %s == %s", conditionKeyValue, condition.Value[0])
Expand Down
2 changes: 2 additions & 0 deletions plugins/wasm-go/extensions/traffic-tag/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const (
Op_In = "in"
Op_NotIn = "not_in"
Op_Percent = "percentage"
Op_Exists = "exists"
Op_NotExists = "not_exists"
TotalWeight = 100
)

Expand Down
126 changes: 126 additions & 0 deletions plugins/wasm-go/extensions/traffic-tag/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,49 @@ var percentageConditionConfig = func() json.RawMessage {
return data
}()

// 测试配置:Exists 条件配置
var existsConditionConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"conditionGroups": []map[string]interface{}{
{
"headerName": "X-Traffic-Tag",
"headerValue": "exists-match",
"logic": "and",
"conditions": []map[string]interface{}{
{
"conditionType": "header",
"key": "X-User-ID",
"operator": "exists",
// exists 操作符不需要 value 字段
},
},
},
},
})
return data
}()

// 测试配置:NotExists 条件配置
var notExistsConditionConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"conditionGroups": []map[string]interface{}{
{
"headerName": "X-Traffic-Tag",
"headerValue": "not-exists-match",
"logic": "and",
"conditions": []map[string]interface{}{
{
"conditionType": "header",
"key": "X-Debug-Mode",
"operator": "not_exists",
},
},
},
},
})
return data
}()

func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// 测试基本条件组配置解析
Expand Down Expand Up @@ -315,6 +358,39 @@ func TestParseConfig(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, config)
})

// 测试存在/不存在配置解析
t.Run("percentage condition config", func(t *testing.T) {
host, status := test.NewTestHost(percentageConditionConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)

config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})

// 测试 Exists 条件配置解析
t.Run("exists condition config", func(t *testing.T) {
host, status := test.NewTestHost(existsConditionConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)

config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})

// 测试 NotExists 条件配置解析
t.Run("not_exists condition config", func(t *testing.T) {
host, status := test.NewTestHost(notExistsConditionConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)

config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
})
}

Expand Down Expand Up @@ -556,6 +632,56 @@ func TestOnHttpRequestHeaders(t *testing.T) {

host.CompleteHttp()
})
// 测试 Exists 条件匹配:当 Header 存在时 (期望匹配成功)
t.Run("exists condition match - header present", func(t *testing.T) {
host, status := test.NewTestHost(existsConditionConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)

action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/test"},
{":method", "GET"},
{"X-User-ID", "user123"}, // 存在目标 Header
})

require.Equal(t, types.ActionContinue, action)
requestHeaders := host.GetRequestHeaders()
tagHeaderFound := false
for _, header := range requestHeaders {
if header[0] == "x-traffic-tag" && header[1] == "exists-match" {
tagHeaderFound = true
break
}
}
require.True(t, tagHeaderFound, "Exists-based traffic tag header should be added")
host.CompleteHttp()
})

// 测试 NotExists 条件匹配:当 Header 不存在时 (期望匹配成功)
t.Run("not_exists condition match - header missing", func(t *testing.T) {
host, status := test.NewTestHost(notExistsConditionConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)

action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/test"},
{":method", "GET"},
})

require.Equal(t, types.ActionContinue, action)
requestHeaders := host.GetRequestHeaders()
tagHeaderFound := false
for _, header := range requestHeaders {
if header[0] == "x-traffic-tag" && header[1] == "not-exists-match" {
tagHeaderFound = true
break
}
}
require.True(t, tagHeaderFound, "Exists-based traffic tag header should be added")
host.CompleteHttp()
})
})
}

Expand Down
22 changes: 13 additions & 9 deletions plugins/wasm-go/extensions/traffic-tag/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"

"regexp"

"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/wasm-go/pkg/log"

Expand Down Expand Up @@ -157,13 +156,15 @@ func (c ConditionRule) validate() error {
}

var validOperators = map[string]bool{
Op_Equal: true,
Op_NotEqual: true,
Op_Prefix: true,
Op_In: true,
Op_NotIn: true,
Op_Regex: true,
Op_Percent: true,
Op_Equal: true,
Op_NotEqual: true,
Op_Prefix: true,
Op_In: true,
Op_NotIn: true,
Op_Regex: true,
Op_Percent: true,
Op_Exists: true,
Op_NotExists: true,
}
if !validOperators[c.Operator] {
return fmt.Errorf("invalid operator: '%s'", c.Operator)
Expand All @@ -174,6 +175,9 @@ func (c ConditionRule) validate() error {
}

switch c.Operator {
case Op_Exists, Op_NotExists:
// 不校验Value
return nil
case Op_In, Op_NotIn:
// 至少一个值
if len(c.Value) < 1 {
Expand Down