Files
SamWaf/wafenginecore/wafengine_test.go
T
2025-06-06 10:58:59 +08:00

469 lines
15 KiB
Go

package wafenginecore
import (
"SamWaf/common/zlog"
"SamWaf/global"
"bytes"
"compress/flate"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestReplaceBodyContent(t *testing.T) {
// 测试用例表格
tests := []struct {
name string
inputBody string
oldString string
newString string
wantBody string
wantError bool
contentType string
}{
{
name: "basic replacement",
inputBody: "Hello world",
oldString: "world",
newString: "golang",
wantBody: "Hello golang",
},
{
name: "multiple occurrences",
inputBody: "banana",
oldString: "na",
newString: "no",
wantBody: "banono",
},
{
name: "empty body",
inputBody: "",
oldString: "test",
newString: "demo",
wantBody: "",
},
{
name: "chinese characters",
inputBody: "你好世界",
oldString: "世界",
newString: "Golang",
wantBody: "你好Golang",
},
{
name: "binary data",
inputBody: string([]byte{0x48, 0x65, 0x00, 0x6c, 0x6c, 0x6f}), // 包含null字节
oldString: string([]byte{0x00}),
newString: " ",
wantBody: "He llo",
},
{
name: "case sensitive",
inputBody: "Go is Cool, go is fun",
oldString: "go",
newString: "GO",
wantBody: "Go is Cool, GO is fun",
},
{
name: "json content",
contentType: "application/json",
inputBody: `{"message":"hello"}`,
oldString: "hello",
newString: "hola",
wantBody: `{"message":"hola"}`,
},
}
waf := &WafEngine{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 创建测试请求
req := httptest.NewRequest("POST", "/", bytes.NewBufferString(tt.inputBody))
if tt.contentType != "" {
req.Header.Set("Content-Type", tt.contentType)
}
// 执行替换操作
err := waf.ReplaceBodyContent(req, []string{tt.oldString}, tt.newString)
if (err != nil) != tt.wantError {
t.Fatalf("ReplaceBodyContent() error = %v, wantError %v", err, tt.wantError)
}
// 读取修改后的Body
gotBody, err := io.ReadAll(req.Body)
if err != nil {
t.Fatal("Failed to read modified body:", err)
}
// 验证结果
if string(gotBody) != tt.wantBody {
t.Errorf("Body mismatch\nWant: %q\nGot: %q", tt.wantBody, string(gotBody))
}
// 验证Content-Type是否保留
if ct := req.Header.Get("Content-Type"); ct != tt.contentType {
t.Errorf("Content-Type changed unexpectedly\nWas: %q\nNow: %q", tt.contentType, ct)
}
})
}
}
func TestReplaceURLContent(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
if v := recover(); v != nil {
zlog.Error("error")
}
// 模拟原始请求
req := httptest.NewRequest("GET", "http://origin.com/test%20/scan?q=hello%2520world&n=1%2B1", nil)
waf := &WafEngine{}
// 执行替换:将"test "替换为"demo"
err := waf.ReplaceURLContent(req, []string{"test "}, "demo")
if err != nil {
t.Fatal(err)
}
// 验证路径编码
if req.URL.Path != "/demo/scan" {
t.Errorf("RawPath mismatch: %s", req.URL.RawPath)
}
// 模拟代理服务器接收的请求
proxyReq := &http.Request{
Method: req.Method,
URL: req.URL,
Header: req.Header,
}
// 验证代理看到的URL
if proxyReq.URL.String() != req.URL.String() {
t.Errorf("Proxy URL mismatch: %s", proxyReq.URL.String())
}
}
func TestGetOrgContent(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
// 测试用例
testCases := []struct {
name string
contentType string
contentEncoding string
content string
expectedContent string // 新增:期望解码后的内容
expectedErr bool
}{
{
name: "UTF-8 无压缩",
contentType: "text/html; charset=utf-8",
contentEncoding: "",
content: "<html><body>这是UTF-8编码的内容</body></html>",
expectedContent: "<html><body>这是UTF-8编码的内容</body></html>",
expectedErr: false,
},
{
name: "GBK 无压缩",
contentType: "text/html; charset=gbk",
contentEncoding: "",
// 这里使用GBK编码的字节序列,而不是UTF-8字符串
content: string([]byte{0x3c, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x3c, 0x62, 0x6f, 0x64, 0x79, 0x3e, 0xd5, 0xe2, 0xca, 0xc7, 0x47, 0x42, 0x4b, 0xb1, 0xe0, 0xc2, 0xeb, 0xb5, 0xc4, 0xc4, 0xda, 0xc8, 0xdd, 0x3c, 0x2f, 0x62, 0x6f, 0x64, 0x79, 0x3e, 0x3c, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3e}),
expectedContent: "<html><body>这是GBK编码的内容</body></html>",
expectedErr: false,
},
{
name: "UTF-8 GZIP压缩",
contentType: "text/html; charset=utf-8",
contentEncoding: "gzip",
content: "<html><body>这是GZIP压缩的UTF-8内容</body></html>",
expectedContent: "<html><body>这是GZIP压缩的UTF-8内容</body></html>",
expectedErr: false,
},
{
name: "UTF-8 Deflate压缩",
contentType: "text/html; charset=utf-8",
contentEncoding: "deflate",
content: "<html><body>这是Deflate压缩的UTF-8内容</body></html>",
expectedContent: "<html><body>这是Deflate压缩的UTF-8内容</body></html>",
expectedErr: false,
},
{
name: "无字符集指定",
contentType: "text/html",
contentEncoding: "",
content: "<html><body>没有指定字符集的内容</body></html>",
expectedContent: "<html><body>没有指定字符集的内容</body></html>",
expectedErr: true,
},
{
// 对于不支持的字符集,我们跳过内容比较,只检查是否有错误
name: "不支持的字符集",
contentType: "text/html; charset=iso-8859-1",
contentEncoding: "",
content: "<html><body>使用不常见字符集的内容</body></html>",
expectedContent: "", // 不比较内容
expectedErr: false,
},
{
name: "JSON内容",
contentType: "application/json; charset=utf-8",
contentEncoding: "",
content: `{"message": "这是JSON内容", "code": 200}`,
expectedContent: `{"message": "这是JSON内容", "code": 200}`,
expectedErr: false,
},
{
name: "大量内容",
contentType: "text/html; charset=utf-8",
contentEncoding: "",
content: strings.Repeat("这是一个很长的内容,用于测试大量数据的处理能力。", 10), // 减少重复次数以加快测试
expectedContent: strings.Repeat("这是一个很长的内容,用于测试大量数据的处理能力。", 10),
expectedErr: false,
},
}
for _, tc := range testCases {
tc := tc // 防止闭包问题
t.Run(tc.name, func(t *testing.T) {
// 创建响应
var bodyContent []byte
var err error
// 根据内容编码处理内容
switch tc.contentEncoding {
case "gzip":
var buf bytes.Buffer
gzipWriter := gzip.NewWriter(&buf)
_, err = gzipWriter.Write([]byte(tc.content))
if err != nil {
t.Fatalf("创建gzip内容失败: %v", err)
}
gzipWriter.Close()
bodyContent = buf.Bytes()
case "deflate":
var buf bytes.Buffer
deflateWriter, _ := flate.NewWriter(&buf, flate.DefaultCompression)
_, err = deflateWriter.Write([]byte(tc.content))
if err != nil {
t.Fatalf("创建deflate内容失败: %v", err)
}
deflateWriter.Close()
bodyContent = buf.Bytes()
default:
bodyContent = []byte(tc.content)
}
// 创建HTTP响应
resp := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewBuffer(bodyContent)),
}
resp.Header.Set("Content-Type", tc.contentType)
if tc.contentEncoding != "" {
resp.Header.Set("Content-Encoding", tc.contentEncoding)
}
// 调用测试函数
result, _, err := waf.getOrgContent(resp, false)
// 验证结果
if tc.expectedErr {
if err == nil {
t.Errorf("期望错误但没有发生错误")
}
} else {
if err != nil {
t.Errorf("不期望错误但发生了错误: %v", err)
} else if tc.expectedContent != "" { // 只有当期望内容不为空时才比较
// 对于GBK测试用例,我们需要特殊处理
if tc.name == "GBK 无压缩" {
// 将结果转换为字符串并检查是否包含期望的文本
resultStr := string(result)
if !strings.Contains(resultStr, "这是GBK编码的内容") {
t.Logf("原始内容: %s", tc.content)
t.Logf("解码后内容: %s", resultStr)
t.Errorf("GBK内容解码不正确")
} else {
t.Logf("测试通过: %s", tc.name)
}
} else {
// 对于其他测试用例,直接比较内容
if !strings.Contains(string(result), tc.expectedContent) {
t.Logf("原始内容: %s", tc.content)
t.Logf("解码后内容: %s", string(result))
t.Errorf("内容解码不正确")
} else {
t.Logf("测试通过: %s", tc.name)
}
}
} else {
// 对于不比较内容的测试用例,只记录通过
t.Logf("测试通过: %s (跳过内容比较)", tc.name)
}
}
})
}
}
// 测试Transfer-Encoding: chunked的情况
func TestGetOrgContentWithChunkedEncoding(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
// 创建分块传输的内容
chunkedContent := "10\r\n这是第一个数据块\r\n14\r\n这是第二个数据块内容\r\n0\r\n\r\n"
// 创建HTTP响应
resp := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewBufferString(chunkedContent)),
TransferEncoding: []string{"chunked"},
}
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
// 调用测试函数
result, _, err := waf.getOrgContent(resp, false)
// 验证结果
if err != nil {
t.Errorf("处理chunked编码失败: %v", err)
} else {
t.Logf("Chunked编码处理结果: %s", string(result))
}
}
// 测试响应体为空的情况
func TestGetOrgContentWithEmptyBody(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
// 创建HTTP响应
resp := &http.Response{
StatusCode: 204, // No Content
Header: make(http.Header),
Body: io.NopCloser(bytes.NewBuffer(nil)),
}
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
// 调用测试函数
result, _, err := waf.getOrgContent(resp, false)
// 验证结果
if err != nil {
t.Errorf("处理空响应体失败: %v", err)
} else {
if len(result) != 0 {
t.Errorf("空响应体应返回空内容,但返回了: %s", string(result))
} else {
t.Log("空响应体测试通过")
}
}
}
// 测试错误情况
func TestGetOrgContentWithErrors(t *testing.T) {
t.Parallel()
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
// 初始化WAF引擎
waf := &WafEngine{}
// 测试gzip解压失败
invalidGzip := []byte{0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00}
resp := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewBuffer(invalidGzip)),
}
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
resp.Header.Set("Content-Encoding", "gzip")
// 调用测试函数
_, _, err := waf.getOrgContent(resp, false)
// 验证结果
if err == nil {
t.Errorf("期望无效gzip内容产生错误,但没有错误")
} else {
t.Logf("无效gzip测试通过,错误: %v", err)
}
}
func TestGetOrgContent_MetaAndDoctypeCharset(t *testing.T) {
t.Parallel()
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, "json")
waf := &WafEngine{}
// 1. meta标签指定utf-8
htmlMetaUtf8 := `<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>utf8内容</body></html>`
resp1 := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(htmlMetaUtf8)),
}
resp1.Header.Set("Content-Type", "text/html")
content1, charset1, err1 := waf.getOrgContent(resp1, false)
if err1 != nil || charset1 != "utf-8" || !strings.Contains(string(content1), "utf8内容") {
t.Errorf("meta标签utf-8检测失败: err=%v, charset=%s, content=%s", err1, charset1, string(content1))
}
// 2. meta标签指定gbk
gbkBody := []byte{0x3c, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x3c, 0x68, 0x65, 0x61, 0x64, 0x3e, 0x3c, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x68, 0x74, 0x74, 0x70, 0x2d, 0x65, 0x71, 0x75, 0x69, 0x76, 0x3d, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x54, 0x79, 0x70, 0x65, 0x22, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x67, 0x62, 0x6b, 0x22, 0x3e, 0x3c, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x3e, 0x3c, 0x62, 0x6f, 0x64, 0x79, 0x3e, 0xd5, 0xe2, 0xca, 0xc7, 0x47, 0x42, 0x4b, 0xb1, 0xe0, 0xc2, 0xeb, 0xb5, 0xc4, 0xc4, 0xda, 0xc8, 0xdd, 0x3c, 0x2f, 0x62, 0x6f, 0x64, 0x79, 0x3e, 0x3c, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3e}
resp2 := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(gbkBody)),
}
resp2.Header.Set("Content-Type", "text/html")
content2, charset2, err2 := waf.getOrgContent(resp2, false)
if err2 != nil || charset2 != "gbk" || !strings.Contains(string(content2), "这是GBK编码的内容") {
t.Errorf("meta标签gbk检测失败: err=%v, charset=%s, content=%s", err2, charset2, string(content2))
}
// 3. DOCTYPE声明xhtml1-transitional.dtd
htmlDoctype := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head></head><body>doctype内容</body></html>`
resp3 := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(htmlDoctype)),
}
resp3.Header.Set("Content-Type", "text/html")
content3, charset3, err3 := waf.getOrgContent(resp3, false)
if err3 != nil || charset3 != "utf-8" || !strings.Contains(string(content3), "doctype内容") {
t.Errorf("DOCTYPE声明检测失败: err=%v, charset=%s, content=%s", err3, charset3, string(content3))
}
// 4. meta标签未知编码
htmlMetaUnknown := `<html><head><meta charset="unknown-charset"></head><body>未知编码</body></html>`
resp4 := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(htmlMetaUnknown)),
}
resp4.Header.Set("Content-Type", "text/html")
_, _, err4 := waf.getOrgContent(resp4, false)
if err4 == nil || !strings.Contains(err4.Error(), "编码检测不确定") {
t.Errorf("未知meta编码检测失败,期望返回错误,实际: %v", err4)
}
}