fix: GET query parameter names disagreed with the docs, silently returning empty data

#1008
This commit is contained in:
samwaf
2026-10-15 10:02:34 +08:00
parent 49726fe7ed
commit 0638f61dca
10 changed files with 358 additions and 76 deletions
+158
View File
@@ -0,0 +1,158 @@
package api
import (
"SamWaf/model/request"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
// GET 接口的 query 绑定历史上只认 Go 字段名(大写),而 Swagger 与产品内文档一律写小写,
// 外部调用方按文档写就会静默拿到零值结构体(issue #1008)。这里用真实 gin 绑定行为做守护。
func bindQuery(t *testing.T, query string, out interface{}) error {
t.Helper()
gin.SetMode(gin.TestMode)
r := gin.New()
var bindErr error
r.GET("/probe", func(c *gin.Context) { bindErr = c.ShouldBind(out) })
target := "/probe"
if query != "" {
target += "?" + query
}
r.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", target, nil))
return bindErr
}
func TestGetQueryBindingUsesLowercaseName(t *testing.T) {
t.Run("host detail", func(t *testing.T) {
var req request.WafHostDetailReq
if err := bindQuery(t, "code=abc", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "abc" {
t.Fatalf("lowercase code not bound, got %q", req.CODE)
}
})
t.Run("host del", func(t *testing.T) {
var req request.WafHostDelReq
if err := bindQuery(t, "code=abc", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "abc" {
t.Fatalf("lowercase code not bound, got %q", req.CODE)
}
})
t.Run("host guard status", func(t *testing.T) {
var req request.WafHostGuardStatusReq
if err := bindQuery(t, "code=abc&guard_status=1", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "abc" || req.GUARD_STATUS != 1 {
t.Fatalf("guard status not bound: %+v", req)
}
})
t.Run("host start status", func(t *testing.T) {
var req request.WafHostStartStatusReq
if err := bindQuery(t, "code=abc&start_status=1", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "abc" || req.START_STATUS != 1 {
t.Fatalf("start status not bound: %+v", req)
}
})
t.Run("rule detail", func(t *testing.T) {
var req request.WafRuleDetailReq
if err := bindQuery(t, "code=rule001", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "rule001" {
t.Fatalf("lowercase code not bound, got %q", req.CODE)
}
})
t.Run("rule del", func(t *testing.T) {
var req request.WafRuleDelReq
if err := bindQuery(t, "code=rule001", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "rule001" {
t.Fatalf("lowercase code not bound, got %q", req.CODE)
}
})
t.Run("rule status", func(t *testing.T) {
var req request.WafRuleStatusReq
if err := bindQuery(t, "code=rule001&rule_status=1", &req); err != nil {
t.Fatalf("bind: %v", err)
}
if req.CODE != "rule001" || req.RULE_STATUS != 1 {
t.Fatalf("rule status not bound: %+v", req)
}
})
}
// 关防御/停服务/禁规则传的是 0,不能被当成缺参
func TestGetQueryBindingAllowsZeroStatus(t *testing.T) {
var guard request.WafHostGuardStatusReq
if err := bindQuery(t, "code=abc&guard_status=0", &guard); err != nil {
t.Fatalf("guard_status=0 should bind: %v", err)
}
if guard.GUARD_STATUS != 0 || guard.CODE != "abc" {
t.Fatalf("unexpected: %+v", guard)
}
var start request.WafHostStartStatusReq
if err := bindQuery(t, "code=abc&start_status=0", &start); err != nil {
t.Fatalf("start_status=0 should bind: %v", err)
}
var rule request.WafRuleStatusReq
if err := bindQuery(t, "code=rule001&rule_status=0", &rule); err != nil {
t.Fatalf("rule_status=0 should bind: %v", err)
}
}
// 大写参数名是历史写法,改动后不再生效(缺 code 直接报错,不再静默返回空数据)
func TestGetQueryBindingRejectsLegacyUppercaseAndMissing(t *testing.T) {
var detail request.WafHostDetailReq
if err := bindQuery(t, "CODE=abc", &detail); err == nil {
t.Fatal("uppercase CODE should no longer satisfy the required binding")
}
if detail.CODE != "" {
t.Fatalf("uppercase CODE unexpectedly bound: %q", detail.CODE)
}
if err := bindQuery(t, "", &detail); err == nil {
t.Fatal("missing code should fail binding")
}
var guard request.WafHostGuardStatusReq
if err := bindQuery(t, "guard_status=1", &guard); err == nil {
t.Fatal("missing code should fail binding")
}
guard = request.WafHostGuardStatusReq{}
if err := bindQuery(t, "code=abc&GUARD_STATUS=1", &guard); err != nil {
t.Fatalf("uppercase GUARD_STATUS should be ignored: %v", err)
}
if guard.GUARD_STATUS != 0 {
t.Fatalf("uppercase GUARD_STATUS unexpectedly bound: %d", guard.GUARD_STATUS)
}
}
// 这几个 req 同时被 POST(JSON) 用,必须保持 json 绑定不受影响
func TestJSONBodyBindingStillWorks(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
var req request.WafHostGuardStatusReq
r.POST("/probe", func(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.String(500, err.Error())
return
}
c.String(200, req.CODE)
})
httpReq := httptest.NewRequest("POST", "/probe", strings.NewReader(`{"code":"abc","guard_status":1}`))
httpReq.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, httpReq)
if rec.Code != 200 || rec.Body.String() != "abc" {
t.Fatalf("json bind broken: %d %s", rec.Code, rec.Body.String())
}
}
+4 -2
View File
@@ -528,7 +528,8 @@ func (w *WafHostAPi) ModifyHostApi(c *gin.Context) {
// @Tags 网站防护-主机管理
// @Accept json
// @Produce json
// @Param data body request.WafHostGuardStatusReq true "防御状态参数"
// @Param code query string true "主机唯一编码"
// @Param guard_status query int true "防御状态 1开启 0关闭"
// @Success 200 {object} response.Response "状态更新成功"
// @Security ApiKeyAuth
// @Router /wafhost/host/guardstatus [get]
@@ -602,7 +603,8 @@ func (w *WafHostAPi) ModifyAllGuardStatusApi(c *gin.Context) {
// @Tags 网站防护-主机管理
// @Accept json
// @Produce json
// @Param data body request.WafHostStartStatusReq true "启动状态参数"
// @Param code query string true "主机唯一编码"
// @Param start_status query int true "启动状态 1启动 0停止"
// @Success 200 {object} response.Response "状态更新成功"
// @Security ApiKeyAuth
// @Router /wafhost/host/startstatus [get]
+3 -3
View File
@@ -102,7 +102,7 @@ func TestLogDetailAPIsHandleErrorsAndLegacyIDs(t *testing.T) {
{"invalid", "req_bad\nvalue", "local_log.db", "格式不正确", response.ERROR},
} {
t.Run(handler.name+"/"+format+"/"+tc.name, func(t *testing.T) {
query := url.Values{"REQ_UUID": {tc.id}, "current_db_name": {tc.shard}, "output_format": {format}}
query := url.Values{"req_uuid": {tc.id}, "current_db_name": {tc.shard}, "output_format": {format}}
got := callLogTestAPI(t, handler.call, http.MethodGet, "/t?"+query.Encode(), "")
if got.Code != tc.wantCode || !strings.Contains(got.Msg, tc.wantMsg) {
t.Fatalf("expected code=%d msg contains %q, got %+v", tc.wantCode, tc.wantMsg, got)
@@ -115,7 +115,7 @@ func TestLogDetailAPIsHandleErrorsAndLegacyIDs(t *testing.T) {
} else if handler.name == "detail" {
data, ok := got.Data.(map[string]interface{})
if !ok || data["req_uuid"] != "req_nonstandard_0" {
t.Fatalf("REQ_UUID form binding failed: %+v", got.Data)
t.Fatalf("req_uuid form binding failed: %+v", got.Data)
}
} else if data, ok := got.Data.(string); !ok || !strings.Contains(data, "GET") || strings.Contains(data, "curl -X ") {
t.Fatalf("copy should contain a populated request: %+v", got.Data)
@@ -128,7 +128,7 @@ func TestLogDetailAPIsHandleErrorsAndLegacyIDs(t *testing.T) {
t.Fatal(err)
}
for _, handler := range []gin.HandlerFunc{api.GetDetailApi, api.GetHttpCopyMaskApi} {
got := callLogTestAPI(t, handler, http.MethodGet, "/t?REQ_UUID=req_nonstandard_0&current_db_name=local_log.db&output_format=curl", "")
got := callLogTestAPI(t, handler, http.MethodGet, "/t?req_uuid=req_nonstandard_0&current_db_name=local_log.db&output_format=curl", "")
if got.Code != response.ERROR || !strings.Contains(got.Msg, "查询日志详情失败") {
t.Fatalf("read failure must propagate: %+v", got)
}
+59 -59
View File
@@ -74,14 +74,14 @@ var routeDetailMap = map[string]routeDetail{
ParamExample: `?code=abc123`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"POST /api/v1/wafhost/host/guardstatus": {
"GET /api/v1/wafhost/host/guardstatus": {
Description: "修改主机防御状态(开启/关闭防御)",
ParamExample: `{"code":"abc123","guard_status":1}`,
ParamExample: `?code=abc123&guard_status=1`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"POST /api/v1/wafhost/host/startstatus": {
"GET /api/v1/wafhost/host/startstatus": {
Description: "修改主机启动状态(启动/停止)",
ParamExample: `{"code":"abc123","start_status":1}`,
ParamExample: `?code=abc123&start_status=1`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
@@ -111,85 +111,85 @@ var routeDetailMap = map[string]routeDetail{
ParamExample: `?code=rule001`,
ResponseExample: `{"code":0,"data":{"code":"rule001","rule_json":"{...}","rule_status":1},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/rule/status": {
"GET /api/v1/wafhost/rule/rulestatus": {
Description: "修改WAF规则启用/禁用状态",
ParamExample: `{"code":"rule001","rule_status":1}`,
ParamExample: `?code=rule001&rule_status=1`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 网站防护-IP黑名单 ========
"POST /api/v1/wafhost/blockip/list": {
"POST /api/v1/wafhost/ipblock/list": {
Description: "获取IP黑名单列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"abc123","ip":""}`,
ResponseExample: `{"code":0,"data":{"list":[{"id":"xxx","ip":"1.2.3.4","host_code":"abc123"}],"total":1},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/blockip/add": {
"POST /api/v1/wafhost/ipblock/add": {
Description: "新增IP黑名单",
ParamExample: `{"host_code":"abc123","ip":"1.2.3.4","remarks":"恶意IP"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"POST /api/v1/wafhost/blockip/edit": {
"POST /api/v1/wafhost/ipblock/edit": {
Description: "编辑IP黑名单",
ParamExample: `{"id":"xxx","host_code":"abc123","ip":"1.2.3.5","remarks":"更新备注"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/wafhost/blockip/del": {
"GET /api/v1/wafhost/ipblock/del": {
Description: "删除IP黑名单",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 网站防护-IP白名单 ========
"POST /api/v1/wafhost/allowip/list": {
"POST /api/v1/wafhost/ipwhite/list": {
Description: "获取IP白名单列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"abc123","ip":""}`,
ResponseExample: `{"code":0,"data":{"list":[{"id":"xxx","ip":"192.168.1.1","host_code":"abc123"}],"total":1},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/allowip/add": {
"POST /api/v1/wafhost/ipwhite/add": {
Description: "新增IP白名单",
ParamExample: `{"host_code":"abc123","ip":"192.168.1.100","remarks":"办公室IP"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"POST /api/v1/wafhost/allowip/edit": {
"POST /api/v1/wafhost/ipwhite/edit": {
Description: "编辑IP白名单",
ParamExample: `{"id":"xxx","host_code":"abc123","ip":"192.168.1.101","remarks":"更新"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/wafhost/allowip/del": {
"GET /api/v1/wafhost/ipwhite/del": {
Description: "删除IP白名单",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 网站防护-URL黑名单 ========
"POST /api/v1/wafhost/blockurl/list": {
"POST /api/v1/wafhost/urlblock/list": {
Description: "获取URL黑名单列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"abc123"}`,
ResponseExample: `{"code":0,"data":{"list":[],"total":0},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/blockurl/add": {
"POST /api/v1/wafhost/urlblock/add": {
Description: "新增URL黑名单",
ParamExample: `{"host_code":"abc123","url":"/admin/","remarks":"禁止访问后台"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/wafhost/blockurl/del": {
"GET /api/v1/wafhost/urlblock/del": {
Description: "删除URL黑名单",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 网站防护-URL白名单 ========
"POST /api/v1/wafhost/allowurl/list": {
"POST /api/v1/wafhost/urlwhite/list": {
Description: "获取URL白名单列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"abc123"}`,
ResponseExample: `{"code":0,"data":{"list":[],"total":0},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/allowurl/add": {
"POST /api/v1/wafhost/urlwhite/add": {
Description: "新增URL白名单(绕过WAF检测)",
ParamExample: `{"host_code":"abc123","url":"/api/upload","remarks":"上传接口"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/wafhost/allowurl/del": {
"GET /api/v1/wafhost/urlwhite/del": {
Description: "删除URL白名单",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
@@ -218,28 +218,28 @@ var routeDetailMap = map[string]routeDetail{
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"","src_ip":"","action":"","unix_add_time_begin":"2026-01-01 00:00:00","unix_add_time_end":"2026-12-31 23:59:59"}`,
ResponseExample: `{"code":0,"data":{"list":[{"req_uuid":"xxx","src_ip":"1.2.3.4","rule":"SQL注入","action":"block","time_str":"2026-03-06 10:00:00"}],"total":100},"msg":"查询成功"}`,
},
"POST /api/v1/waflog/attack/detail": {
"GET /api/v1/waflog/attack/detail": {
Description: "获取攻击日志详情",
ParamExample: `{"req_uuid":"xxx-yyy-zzz","current_db_name":"local_log"}`,
ParamExample: `?req_uuid=xxx-yyy-zzz&current_db_name=local_log`,
ResponseExample: `{"code":0,"data":{"req_uuid":"xxx","src_ip":"1.2.3.4","method":"POST","url":"/login","body":"..."},"msg":"查询成功"}`,
},
// ======== 统计-数据统计 ========
"POST /api/v1/stat/dayrangecount": {
"GET /api/v1/wafstatsumdayrange": {
Description: "按日期范围统计访问量/拦截量",
ParamExample: `{"begin_date":"2026-01-01","end_date":"2026-03-06","host_code":""}`,
ParamExample: `?start_day=2026-01-01&end_day=2026-03-06`,
ResponseExample: `{"code":0,"data":{"list":[{"date":"2026-03-06","total":1000,"block":50}]},"msg":"查询成功"}`,
},
"POST /api/v1/stat/ipcount": {
"GET /api/v1/wafstatsumdaytopiprange": {
Description: "统计攻击IP排行",
ParamExample: `{"begin_date":"2026-01-01","end_date":"2026-03-06","host_code":"","top":10}`,
ParamExample: `?start_day=2026-01-01&end_day=2026-03-06`,
ResponseExample: `{"code":0,"data":{"list":[{"src_ip":"1.2.3.4","count":500}]},"msg":"查询成功"}`,
},
// ======== 系统日志 ========
"POST /api/v1/syslog/list": {
"GET /api/v1/sys_log/list": {
Description: "获取系统操作日志列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"op_type":"","op_content":""}`,
ParamExample: `?pageIndex=1&pageSize=10&op_type=&op_content=`,
ResponseExample: `{"code":0,"data":{"list":[{"op_type":"login","op_content":"用户登录","time_str":"2026-03-06 10:00:00"}],"total":50},"msg":"查询成功"}`,
},
@@ -266,31 +266,31 @@ var routeDetailMap = map[string]routeDetail{
ParamExample: `{"pageIndex":1,"pageSize":50,"config_type":""}`,
ResponseExample: `{"code":0,"data":{"list":[{"config_type":"ssl","config_key":"sslorder_expire_day","config_value":"30"}],"total":10},"msg":"查询成功"}`,
},
"POST /api/v1/systemconfig/updateitem": {
"POST /api/v1/systemconfig/editByItem": {
Description: "更新单个系统配置项",
ParamExample: `{"config_type":"ssl","config_key":"sslorder_expire_day","config_value":"15"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 系统信息 ========
"GET /api/v1/sysinfo/info": {
"GET /api/v1/sysinfo/runtimeinfo": {
Description: "获取系统运行信息(CPU、内存、版本等)",
ParamExample: `(无需参数)`,
ResponseExample: `{"code":0,"data":{"cpu_percent":10.5,"mem_percent":40.2,"version":"1.x.x"},"msg":"查询成功"}`,
},
// ======== SSL证书配置 ========
"POST /api/v1/wafhost/sslconfig/list": {
"POST /api/v1/sslconfig/list": {
Description: "获取SSL证书列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10}`,
ResponseExample: `{"code":0,"data":{"list":[{"id":"xxx","domain":"example.com","expire_time":"2027-01-01"}],"total":1},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/sslconfig/add": {
"POST /api/v1/sslconfig/add": {
Description: "上传新增SSL证书",
ParamExample: `{"domain":"example.com","cert_content":"-----BEGIN CERTIFICATE-----...","key_content":"-----BEGIN PRIVATE KEY-----..."}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/wafhost/sslconfig/del": {
"GET /api/v1/sslconfig/del": {
Description: "删除SSL证书",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
@@ -343,17 +343,17 @@ var routeDetailMap = map[string]routeDetail{
},
// ======== 防火墙IP封禁 ========
"POST /api/v1/firewallipblock/list": {
"POST /api/v1/firewall/ipblock/list": {
Description: "获取防火墙IP封禁规则列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10}`,
ResponseExample: `{"code":0,"data":{"list":[],"total":0},"msg":"查询成功"}`,
},
"POST /api/v1/firewallipblock/add": {
"POST /api/v1/firewall/ipblock/add": {
Description: "新增防火墙IP封禁规则",
ParamExample: `{"ip":"1.2.3.4","remarks":"攻击者","expire_time":""}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
"GET /api/v1/firewallipblock/del": {
"GET /api/v1/firewall/ipblock/del": {
Description: "删除防火墙IP封禁规则",
ParamExample: `?id=xxx`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
@@ -426,31 +426,31 @@ var routeDetailMap = map[string]routeDetail{
},
// ======== IP失败封禁 ========
"POST /api/v1/wafhost/ipfailure/list": {
Description: "获取IP失败封禁规则列表(分页)",
ParamExample: `{"pageIndex":1,"pageSize":10,"host_code":"abc123"}`,
"GET /api/v1/wafhost/ipfailure/config": {
Description: "获取指定站点的IP失败封禁策略配置",
ParamExample: `?host_code=abc123`,
ResponseExample: `{"code":0,"data":{"host_code":"abc123","url":"/login","fail_count":5},"msg":"查询成功"}`,
},
"GET /api/v1/wafhost/ipfailure/baniplist": {
Description: "获取IP失败封禁命中的IP列表",
ParamExample: `?host_code=abc123`,
ResponseExample: `{"code":0,"data":{"list":[],"total":0},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/ipfailure/add": {
Description: "新增IP失败次数封禁规则",
"POST /api/v1/wafhost/ipfailure/config": {
Description: "设置IP失败次数封禁策略(按站点)",
ParamExample: `{"host_code":"abc123","url":"/login","fail_count":5,"time_window":300,"block_time":3600,"remarks":"登录失败封禁"}`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== WAF引擎 ========
"GET /api/v1/wafhost/engine/status": {
Description: "获取WAF引擎运行状态",
ParamExample: `(无需参数)`,
ResponseExample: `{"code":0,"data":{"status":"running","host_count":5},"msg":"查询成功"}`,
},
"POST /api/v1/wafhost/engine/restart": {
"GET /api/v1/resetWAF": {
Description: "重启WAF引擎(重新加载所有规则和配置)",
ParamExample: `{}`,
ParamExample: `(无需参数)`,
ResponseExample: `{"code":0,"data":{},"msg":"操作成功"}`,
},
// ======== 系统监控 ========
"GET /api/v1/monitor/info": {
"GET /api/v1/monitor/system_info": {
Description: "获取实时系统监控数据(CPU、内存、网络流量)",
ParamExample: `(无需参数)`,
ResponseExample: `{"code":0,"data":{"cpu":15.2,"mem":40.5,"net_in":1024,"net_out":2048},"msg":"查询成功"}`,
@@ -473,16 +473,16 @@ var routeDetailMap = map[string]routeDetail{
var routeModuleMap = map[string]string{
"/api/v1/wafhost/host": "网站防护-主机管理",
"/api/v1/wafhost/rule": "网站防护-规则管理",
"/api/v1/wafhost/allowip": "网站防护-IP白名单",
"/api/v1/wafhost/allowurl": "网站防护-URL白名单",
"/api/v1/wafhost/ipwhite": "网站防护-IP白名单",
"/api/v1/wafhost/urlwhite": "网站防护-URL白名单",
"/api/v1/wafhost/ldpurl": "网站防护-隐私保护URL",
"/api/v1/wafhost/blockip": "网站防护-IP黑名单",
"/api/v1/wafhost/blockurl": "网站防护-URL黑名单",
"/api/v1/wafhost/ipblock": "网站防护-IP黑名单",
"/api/v1/wafhost/urlblock": "网站防护-URL黑名单",
"/api/v1/wafhost/anticc": "网站防护-CC防护",
"/api/v1/wafhost/ipfailure": "网站防护-IP失败封禁",
"/api/v1/wafhost/sensitive": "网站防护-敏感词",
"/api/v1/wafhost/loadbalance": "网站防护-负载均衡",
"/api/v1/wafhost/sslconfig": "SSL-证书配置",
"/api/v1/sslconfig": "SSL-证书配置",
"/api/v1/wafhost/sslorder": "SSL-证书申请",
"/api/v1/wafhost/sslexpire": "SSL-证书过期",
"/api/v1/wafhost/httpauthbase": "网站防护-HTTP认证",
@@ -490,11 +490,11 @@ var routeModuleMap = map[string]string{
"/api/v1/wafhost/cacherule": "网站防护-缓存规则",
"/api/v1/wafhost/otp": "安全-OTP双因素",
"/api/v1/waflog/attack": "日志-攻击日志",
"/api/v1/stat": "统计-数据统计",
"/api/v1/wafhost/engine": "引擎-WAF引擎",
"/api/v1/wafstat": "统计-数据统计",
"/api/v1/resetWAF": "引擎-WAF引擎",
"/api/v1/account": "账号管理",
"/api/v1/accountlog": "账号日志",
"/api/v1/syslog": "系统日志",
"/api/v1/account_log": "账号日志",
"/api/v1/sys_log": "系统日志",
"/api/v1/sysinfo": "系统信息",
"/api/v1/systemconfig": "系统配置",
"/api/v1/wafcommon": "公共接口",
@@ -515,7 +515,7 @@ var routeModuleMap = map[string]string{
"/api/v1/notify/channel": "通知-渠道",
"/api/v1/notify/subscription": "通知-订阅",
"/api/v1/notify/log": "通知-日志",
"/api/v1/firewallipblock": "防火墙-IP封禁",
"/api/v1/firewall/ipblock": "防火墙-IP封禁",
"/api/v1/plugin": "插件管理",
"/api/v1/logfilewrite": "日志文件写入",
"/api/v1/iplocation": "IP地址库",
+88
View File
@@ -0,0 +1,88 @@
package api
import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"strconv"
"testing"
)
// 开放平台文档表里的 key 必须与 router/ 实际注册的 method+path 对得上。
// 历史上有 34 条写错(方法写成 POST、路径前缀停留在旧命名),外部调用方照文档调会直接 404。
func registeredRoutes(t *testing.T) map[string]bool {
t.Helper()
files, err := filepath.Glob(filepath.Join("..", "router", "*.go"))
if err != nil || len(files) == 0 {
t.Fatalf("router 目录不可读: %v", err)
}
routes := map[string]bool{}
fset := token.NewFileSet()
for _, f := range files {
file, err := parser.ParseFile(fset, f, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", f, err)
}
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok || len(call.Args) < 2 {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
method := sel.Sel.Name
switch method {
case "GET", "POST", "PUT", "DELETE", "PATCH":
default:
return true
}
lit, ok := call.Args[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
path, err := strconv.Unquote(lit.Value)
if err != nil {
return true
}
routes[method+" "+path] = true
return true
})
}
return routes
}
func TestOPlatformDocEntriesMatchRoutes(t *testing.T) {
routes := registeredRoutes(t)
var missing []string
for key := range routeDetailMap {
if !routes[key] {
missing = append(missing, key)
}
}
if len(missing) > 0 {
t.Fatalf("开放平台文档表里的 %d 条接口在 router/ 中不存在:%v", len(missing), missing)
}
}
// GET 条目的参数示例必须是 ?query 形式:buildCurlExample 靠此前缀拼 curl,JSON 体在 GET 上不成立
func TestOPlatformDocGetEntriesUseQueryExample(t *testing.T) {
var bad []string
for key, detail := range routeDetailMap {
if len(key) < 4 || key[:4] != "GET " {
continue
}
if detail.ParamExample == "(无需参数)" {
continue
}
if len(detail.ParamExample) == 0 || detail.ParamExample[0] != '?' {
bad = append(bad, key)
}
}
if len(bad) > 0 {
t.Fatalf("以下 GET 条目的参数示例不是 query 形式:%v", bad)
}
}
+2 -1
View File
@@ -439,7 +439,8 @@ func (w *WafRuleAPi) FormatRuleApi(c *gin.Context) {
// @Tags 网站防护-规则管理
// @Accept json
// @Produce json
// @Param data body request.WafRuleStatusReq true "状态参数"
// @Param code query string true "规则唯一编码"
// @Param rule_status query int true "规则状态 1启用 0禁用"
// @Success 200 {object} response.Response "状态更新成功"
// @Security ApiKeyAuth
// @Router /wafhost/rule/rulestatus [get]
+2 -2
View File
@@ -4,9 +4,9 @@ import "SamWaf/model/common/request"
type WafAttackLogDetailReq struct {
// detail/httpcopymask 为 GET 请求,需 form tag 才能从 query 绑定;
// 前端发送的 query 键为 current_db_name / REQ_UUID / output_format
// query 键与 json tag 同名(全小写),Swagger 与产品内文档按此书写
CurrrentDbName string `json:"current_db_name" form:"current_db_name"`
REQ_UUID string `json:"req_uuid" form:"REQ_UUID" binding:"required,max=64"`
REQ_UUID string `json:"req_uuid" form:"req_uuid" binding:"required,max=64"`
OutputFormat string `json:"output_format" form:"output_format"` //输出格式 raw,curl
}
+6 -6
View File
@@ -63,11 +63,11 @@ type WafHostAddReq struct {
}
type WafHostDelReq struct {
CODE string `json:"code"`
CODE string `json:"code" form:"code" binding:"required"`
}
type WafHostDetailReq struct {
CODE string `json:"code"`
CODE string `json:"code" form:"code" binding:"required"`
}
type WafHostEditReq struct {
@@ -142,8 +142,8 @@ type WafHostCheckPortsReq struct {
}
type WafHostGuardStatusReq struct {
CODE string `json:"code"`
GUARD_STATUS int `json:"guard_status"` //防御状态 1 是开启防御 0 是防御关闭
CODE string `json:"code" form:"code" binding:"required"`
GUARD_STATUS int `json:"guard_status" form:"guard_status"` //防御状态 1 是开启防御 0 是防御关闭
}
type WafHostSearchReq struct {
@@ -161,8 +161,8 @@ type WafHostSearchReq struct {
}
type WafHostStartStatusReq struct {
CODE string `json:"code"`
START_STATUS int `json:"start_status"` //启动状态
CODE string `json:"code" form:"code" binding:"required"`
START_STATUS int `json:"start_status" form:"start_status"` //启动状态
}
type WafHostAllDomainsReq struct {
+3 -3
View File
@@ -10,10 +10,10 @@ type WafRuleAddReq struct {
RuleStatus int `json:"rule_status"` //规则状态 1 是开启 0 是关闭
}
type WafRuleDelReq struct {
CODE string `json:"code"`
CODE string `json:"code" form:"code" binding:"required"`
}
type WafRuleDetailReq struct {
CODE string `json:"code"`
CODE string `json:"code" form:"code" binding:"required"`
}
type WafRuleEditReq struct {
CODE string `json:"code"`
@@ -44,7 +44,7 @@ type WafRulePreViewReq struct {
}
type WafRuleStatusReq struct {
CODE string `json:"code"`
CODE string `json:"code" form:"code" binding:"required"`
RULE_STATUS int `json:"rule_status" form:"rule_status"` //规则状态 1 是开启 0 是关闭
}
+33
View File
@@ -1031,3 +1031,36 @@ notes:
requirement. One fallback to know: if the scope is set to "only listed paths" but the list is
empty, protection falls back to the whole site with a note in the system log - an incomplete
configuration never silently disables the gate.
- id: v1_3_25_api_query_param_case
version: v1.3.25
kind: notice
level: high
page: /oplatform/doc
doc: https://doc.samwaf.com/quickstart/Update.html
apply:
type: navigate
zh:
title: 通过 API 调用时,查询参数名统一为全小写
detail: >-
以下接口的查询参数名统一改为与文档一致的全小写形式,请检查你的对接脚本:
网站详情 / 删除 / 防御开关 / 启停开关(wafhost/host/detail、host/del、host/guardstatus、
host/startstatus),规则详情 / 删除 / 启用开关(wafhost/rule/detail、rule/del、rule/rulestatus),
攻击日志详情与报文复制(waflog/attack/detail、waflog/attack/httpcopymask)。
即 CODE 改为 code、REQ_UUID 改为 req_uuid、GUARD_STATUS 改为 guard_status、
START_STATUS 改为 start_status;rule_status 本就是小写,不变。
本次同时修正了开放平台文档中一批方法与路径标注有误的接口(IP/URL 黑白名单、
SSL 证书、主机防火墙 IP 封禁、统计与系统日志等),请以页面上的最新说明为准。
en:
title: API query parameter names are now all lower case
detail: >-
The query parameter names of the following endpoints now match the documentation exactly,
all lower case - check your integration scripts: site detail / delete / protection toggle /
start-stop toggle (wafhost/host/detail, host/del, host/guardstatus, host/startstatus),
rule detail / delete / enable toggle (wafhost/rule/detail, rule/del, rule/rulestatus),
and attack log detail plus HTTP copy-mask (waflog/attack/detail, waflog/attack/httpcopymask).
In other words CODE becomes code, REQ_UUID becomes req_uuid, GUARD_STATUS becomes guard_status,
and START_STATUS becomes start_status; rule_status was already lower case and is unchanged.
A set of endpoints whose documented method or path was wrong has also been corrected
(IP/URL block and allow lists, SSL certificates, host firewall IP blocking, statistics and
system logs) - use the updated descriptions on the page.