Files
higress/plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go

1280 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"hash"
"strings"
"testing"
"time"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/stretchr/testify/require"
)
// 生成有效的 HMAC 签名 - 与插件中的 generateSigningString 方法保持一致
func generateValidSignature(accessKey, secretKey, algorithm, requestMethod, requestURI string, headers []string, headerValues map[string]string) string {
// 构造签名字符串,严格按照插件中的 generateSigningString 方法实现
var signingStringItems []string
// 第一行是 keyId
signingStringItems = append(signingStringItems, accessKey)
if len(headers) > 0 {
for _, h := range headers {
if h == "@request-target" {
// 注意这里使用原始请求方法和URI
requestTarget := fmt.Sprintf("%s %s", requestMethod, requestURI)
signingStringItems = append(signingStringItems, requestTarget)
} else {
if value, ok := headerValues[h]; ok {
signingStringItems = append(signingStringItems, fmt.Sprintf("%s: %s", h, value))
}
}
}
}
// 签名字符串需要以换行符结尾
signingString := strings.Join(signingStringItems, "\n") + "\n"
// 生成 HMAC 签名
var mac hash.Hash
switch algorithm {
case "hmac-sha1":
mac = hmac.New(sha1.New, []byte(secretKey))
case "hmac-sha256":
mac = hmac.New(sha256.New, []byte(secretKey))
case "hmac-sha512":
mac = hmac.New(sha512.New, []byte(secretKey))
default:
mac = hmac.New(sha256.New, []byte(secretKey))
}
mac.Write([]byte(signingString))
signature := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(signature)
}
// 生成 Authorization 头
func generateAuthorizationHeader(accessKey, secretKey, algorithm, requestMethod, requestURI string, headers []string, headerValues map[string]string) string {
signature := generateValidSignature(accessKey, secretKey, algorithm, requestMethod, requestURI, headers, headerValues)
header := fmt.Sprintf(`Signature keyId="%s",algorithm="%s",signature="%s"`, accessKey, algorithm, signature)
if len(headers) > 0 {
header += fmt.Sprintf(`,headers="%s"`, strings.Join(headers, " "))
}
return header
}
// 辅助函数:生成有效的认证头
func generateValidAuthHeaderWithDate(dateStr, method, path string) string {
headerValues := map[string]string{
"date": dateStr,
}
return generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", method, path, []string{"@request-target", "date"}, headerValues)
}
// 通用测试配置生成函数
func createConfig(consumers []map[string]interface{}, extra map[string]interface{}) json.RawMessage {
config := map[string]interface{}{
"consumers": consumers,
// 设置 clock_skew 为 0 来跳过时钟偏差校验
"clock_skew": 0,
}
for k, v := range extra {
config[k] = v
}
data, _ := json.Marshal(config)
return data
}
func TestParseGlobalConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
tests := []struct {
name string
config json.RawMessage
expected bool
}{
{
"basic global config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{},
),
true,
},
{
"global auth true config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
),
true,
},
{
"global auth false config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": false,
},
),
true,
},
{
"clock skew config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"clock_skew": 600,
},
),
true,
},
{
"algorithm config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"allowed_algorithms": []string{"hmac-sha256"},
},
),
true,
},
{
"signed headers config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"signed_headers": []string{"host", "date"},
},
),
true,
},
{
"anonymous consumer config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"anonymous_consumer": "anonymous",
},
),
true,
},
{
"invalid config - missing consumers",
createConfig(
[]map[string]interface{}{},
map[string]interface{}{
"global_auth": false,
},
),
false,
},
{
"invalid config - empty consumers",
createConfig(
[]map[string]interface{}{},
map[string]interface{}{
"consumers": []map[string]interface{}{},
},
),
false,
},
{
"invalid config - missing access_key",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"secret_key": "sk1",
},
},
map[string]interface{}{},
),
false,
},
{
"invalid config - missing secret_key",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
},
},
map[string]interface{}{},
),
false,
},
{
"invalid config - duplicate access_key",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak1",
"secret_key": "sk2",
},
},
map[string]interface{}{},
),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, status := test.NewTestHost(tt.config)
defer host.Reset()
if tt.expected {
require.Equal(t, types.OnPluginStartStatusOK, status)
} else {
require.Equal(t, types.OnPluginStartStatusFailed, status)
}
})
}
})
}
func TestParseOverrideRuleConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
tests := []struct {
name string
config json.RawMessage
expected bool
}{
{
"route auth config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"allow": []string{"consumer1"},
},
),
true,
},
{
"domain auth config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"allow": []string{"consumer2"},
},
),
true,
},
{
"invalid config - empty allow list",
createConfig(
[]map[string]interface{}{},
map[string]interface{}{
"allow": []string{},
},
),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, status := test.NewTestHost(tt.config)
defer host.Reset()
if tt.expected {
require.Equal(t, types.OnPluginStartStatusOK, status)
} else {
require.Equal(t, types.OnPluginStartStatusFailed, status)
}
})
}
})
}
func TestParseRuleConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
tests := []struct {
name string
config json.RawMessage
expected bool
}{
{
"route level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_route_": []string{"route-a", "route-b"},
"allow": []string{"consumer1"},
},
{
"_match_route_": []string{"route-c"},
"allow": []string{"consumer2"},
},
},
},
),
true,
},
{
"domain level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_domain_": []string{"*.example.com", "test.com"},
"allow": []string{"consumer2"},
},
{
"_match_domain_": []string{"api.example.com"},
"allow": []string{"consumer1"},
},
},
},
),
true,
},
{
"service level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_service_": []string{"service-a:8080", "service-b"},
"allow": []string{"consumer1"},
},
{
"_match_service_": []string{"service-b:9090"},
"allow": []string{"consumer2"},
},
},
},
),
true,
},
{
"route prefix level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_route_prefix_": []string{"api-", "web-"},
"allow": []string{"consumer1"},
},
{
"_match_route_prefix_": []string{"admin-", "internal-"},
"allow": []string{"consumer2"},
},
},
},
),
true,
},
{
"route and service level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_route_": []string{"route-a"},
"_match_service_": []string{"service-a:8080"},
"allow": []string{"consumer1"},
},
{
"_match_route_": []string{"route-b"},
"_match_service_": []string{"service-b:9090"},
"allow": []string{"consumer2"},
},
},
},
),
true,
},
{
"mixed level config",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
{
"name": "consumer3",
"access_key": "ak3",
"secret_key": "sk3",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_route_": []string{"api-route"},
"allow": []string{"consumer1"},
},
{
"_match_domain_": []string{"*.example.com"},
"allow": []string{"consumer2"},
},
{
"_match_service_": []string{"internal-service:8080"},
"allow": []string{"consumer3"},
},
{
"_match_route_prefix_": []string{"web-"},
"allow": []string{"consumer1", "consumer2"},
},
},
},
),
true,
},
{
"invalid rule config - missing match conditions",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"allow": []string{"consumer1"},
// 缺少匹配条件
},
},
},
),
false,
},
{
"invalid rule config - empty match conditions",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"_rules_": []map[string]interface{}{
{
"_match_route_": []string{},
"allow": []string{"consumer1"},
},
},
},
),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, status := test.NewTestHost(tt.config)
defer host.Reset()
if tt.expected {
require.Equal(t, types.OnPluginStartStatusOK, status)
} else {
require.Equal(t, types.OnPluginStartStatusFailed, status)
}
})
}
})
}
func TestOnHttpRequestHeaders(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
tests := []struct {
name string
config json.RawMessage
headers [][2]string
expectContinue bool
expectError string
}{
{
"missing authorization header",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{},
),
[][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}},
true,
`{"message":"client request can't be validated: missing Authorization header"}`,
},
{
"empty authorization header",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{},
),
[][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", ""}},
true,
`{"message":"client request can't be validated: missing Authorization header"}`,
},
{
"invalid authorization format - missing signature prefix",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
),
[][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", "Bearer token123"}},
true,
`{"message":"client request can't be validated: Authorization header does not start with 'Signature '"}`,
},
{
"invalid authorization format - invalid signature format",
createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
),
[][2]string{{":authority", "example.com"}, {":path", "/api/test"}, {":method", "GET"}, {"authorization", "Signature invalid-format"}},
true,
`{"message":"client request can't be validated: keyId or signature missing"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, status := test.NewTestHost(tt.config)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders(tt.headers)
require.Equal(t, types.ActionContinue, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Equal(t, tt.expectError, string(localResponse.Data))
host.CompleteHttp()
})
}
// 测试有效的凭证情况
t.Run("valid credentials - global auth true, no allow config", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted")
host.CompleteHttp()
})
// 测试无效的凭证(未配置的 access_key
t.Run("invalid credential - not configured access_key", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("unknown_ak", "sk", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse, "Invalid credentials should be rejected")
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Equal(t, `{"message":"client request can't be validated: Invalid keyId"}`, string(localResponse.Data))
host.CompleteHttp()
})
// 测试无效的签名(错误的签名)
t.Run("invalid signature - wrong signature", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
// 故意修改签名
authHeader = strings.Replace(authHeader, "signature=", "signature=wrong", 1)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse, "Invalid signature should be rejected")
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Equal(t, `{"message":"client request can't be validated: keyId or signature missing"}`, string(localResponse.Data))
host.CompleteHttp()
})
// 测试有效的凭证(全局认证关闭,有 allow 配置)
t.Run("valid credentials - global auth false, with allow config", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"allow": []string{"consumer1"},
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted")
host.CompleteHttp()
})
// 测试有效的凭证但不在 allow 列表中的情况
t.Run("valid credentials but not in allow list", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
{
"name": "consumer2",
"access_key": "ak2",
"secret_key": "sk2",
},
},
map[string]interface{}{
"allow": []string{"consumer1"},
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("ak2", "sk2", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted when not in allow list")
host.CompleteHttp()
})
// 测试匿名消费者配置
t.Run("anonymous consumer config", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"anonymous_consumer": "anonymous",
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse(), "Request without credentials should be accepted with anonymous consumer")
host.CompleteHttp()
})
})
}
func TestCompleteFlow(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
t.Run("complete hmac auth flow", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"global_auth": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 1. 测试缺少认证信息的情况
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
})
require.Equal(t, types.ActionContinue, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse, "Request without credentials should be rejected")
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Equal(t, `{"message":"client request can't be validated: missing Authorization header"}`, string(localResponse.Data))
host.CompleteHttp()
// 2. 测试有效认证的情况
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
headerValues := map[string]string{
"date": fixedTime,
}
authHeader := generateAuthorizationHeader("ak1", "sk1", "hmac-sha256", "GET", "/api/test", []string{"@request-target"}, headerValues)
action = host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"authorization", authHeader},
{"date", fixedTime},
})
require.Equal(t, types.ActionContinue, action)
requestHeaders := host.GetRequestHeaders()
require.True(t, test.HasHeaderWithValue(requestHeaders, "X-Mse-Consumer", "consumer1"))
require.Nil(t, host.GetLocalResponse(), "Valid credentials should be accepted")
host.CompleteHttp()
})
})
}
func TestOnHttpRequestBody(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// 测试有效的请求体和摘要
t.Run("valid request body with correct digest", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"validate_request_body": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 准备空请求体的摘要
validBody := []byte(`{"name": "test", "value": 123}`)
correctDigest := calculateBodyDigest(validBody)
// 使用固定的时间值确保一致性
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
// 生成与请求头一致的签名
authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test")
// 先处理请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"authorization", authHeader},
{"date", fixedTime},
{"digest", correctDigest}, // 使用正确的摘要
{"content-type", "application/json"},
})
// 测试有效的请求体
action := host.CallOnHttpRequestBody(validBody)
require.Equal(t, types.ActionContinue, action, "Valid body with correct digest should be accepted")
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue")
localResponse := host.GetLocalResponse()
require.Nil(t, localResponse, "Valid body with correct digest should not be rejected")
host.CompleteHttp()
})
// 测试无效的摘要
t.Run("invalid digest", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"validate_request_body": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 使用固定的时间值确保一致性
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
// 生成与请求头一致的签名
authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test")
// 先调用头部处理
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"authorization", authHeader},
{"date", fixedTime},
{"digest", "SHA-256=invalid-digest"},
{"content-type", "application/json"},
})
// 测试请求体但摘要不匹配
body := []byte(`{"name": "test", "value": 123}`)
action := host.CallOnHttpRequestBody(body)
require.Equal(t, types.ActionContinue, action, "Request with invalid digest should be rejected")
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue")
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse, "Request with invalid digest should be rejected")
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Contains(t, string(localResponse.Data), "Invalid digest", "Error message should indicate invalid digest")
host.CompleteHttp()
})
// 测试缺少摘要头
t.Run("missing digest header", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"validate_request_body": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 使用固定的时间值确保一致性
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
// 生成与请求头一致的签名
authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test")
// 先调用头部处理但不设置digest头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"authorization", authHeader},
{"date", fixedTime},
// 故意不设置digest头
{"content-type", "application/json"},
})
// 测试请求体但缺少摘要头
body := []byte(`{"name": "test", "value": 123}`)
action := host.CallOnHttpRequestBody(body)
require.Equal(t, types.ActionContinue, action, "Request without digest header should be rejected")
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue")
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse, "Request without digest header should be rejected")
require.Equal(t, uint32(401), localResponse.StatusCode)
require.Contains(t, string(localResponse.Data), "Invalid digest", "Error message should indicate invalid digest")
host.CompleteHttp()
})
// 测试未启用请求体验证
t.Run("body validation disabled", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"validate_request_body": false,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 使用固定的时间值确保一致性
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
// 生成与请求头一致的签名
authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test")
// 先调用头部处理
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"authorization", authHeader},
{"date", fixedTime},
{"content-type", "application/json"},
})
// 由于禁用了请求体验证,应该直接继续而不等待请求体
require.Equal(t, types.ActionContinue, action, "Should continue immediately when validate_request_body is false")
host.CompleteHttp()
})
// 测试空请求体
t.Run("empty request body", func(t *testing.T) {
host, status := test.NewTestHost(createConfig(
[]map[string]interface{}{
{
"name": "consumer1",
"access_key": "ak1",
"secret_key": "sk1",
},
},
map[string]interface{}{
"validate_request_body": true,
},
))
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 使用固定的时间值确保一致性
fixedTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
// 准备空请求体的摘要
emptyBody := []byte("")
correctDigest := calculateBodyDigest(emptyBody)
// 生成与请求头一致的签名
authHeader := generateValidAuthHeaderWithDate(fixedTime, "POST", "/api/test")
// 先调用头部处理
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"authorization", authHeader},
{"date", fixedTime},
{"digest", correctDigest}, // 空字符串的SHA-256摘要
{"content-type", "application/json"},
})
// 测试空请求体
action := host.CallOnHttpRequestBody(emptyBody)
require.Equal(t, types.ActionContinue, action, "Empty body with correct digest should be accepted")
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction(), "Stream action should be continue")
localResponse := host.GetLocalResponse()
require.Nil(t, localResponse, "Empty body with correct digest should not be rejected")
host.CompleteHttp()
})
})
}