From 0638f61dca62bf4df0e67ddeab9f0b99697b2d23 Mon Sep 17 00:00:00 2001 From: samwaf Date: Thu, 15 Oct 2026 10:02:34 +0800 Subject: [PATCH] fix: GET query parameter names disagreed with the docs, silently returning empty data #1008 --- api/waf_bind_case_test.go | 158 ++++++++++++++++++++++++++++ api/waf_host.go | 6 +- api/waf_log_test.go | 6 +- api/waf_oplatform_doc.go | 118 ++++++++++----------- api/waf_oplatform_doc_test.go | 88 ++++++++++++++++ api/waf_rule.go | 3 +- model/request/waf_attack_log_req.go | 4 +- model/request/waf_host_req.go | 12 +-- model/request/waf_rule_req.go | 6 +- wafupgradenotice/upgrade_notes.yaml | 33 ++++++ 10 files changed, 358 insertions(+), 76 deletions(-) create mode 100644 api/waf_bind_case_test.go create mode 100644 api/waf_oplatform_doc_test.go diff --git a/api/waf_bind_case_test.go b/api/waf_bind_case_test.go new file mode 100644 index 0000000..992e82b --- /dev/null +++ b/api/waf_bind_case_test.go @@ -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()) + } +} diff --git a/api/waf_host.go b/api/waf_host.go index 8bf44a5..830513e 100644 --- a/api/waf_host.go +++ b/api/waf_host.go @@ -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] diff --git a/api/waf_log_test.go b/api/waf_log_test.go index 9d3a4dc..ec4017f 100644 --- a/api/waf_log_test.go +++ b/api/waf_log_test.go @@ -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¤t_db_name=local_log.db&output_format=curl", "") + got := callLogTestAPI(t, handler, http.MethodGet, "/t?req_uuid=req_nonstandard_0¤t_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) } diff --git a/api/waf_oplatform_doc.go b/api/waf_oplatform_doc.go index 752eb0e..3016a57 100644 --- a/api/waf_oplatform_doc.go +++ b/api/waf_oplatform_doc.go @@ -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¤t_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地址库", diff --git a/api/waf_oplatform_doc_test.go b/api/waf_oplatform_doc_test.go new file mode 100644 index 0000000..8577baf --- /dev/null +++ b/api/waf_oplatform_doc_test.go @@ -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) + } +} diff --git a/api/waf_rule.go b/api/waf_rule.go index 9a4b26b..a2c4cc0 100644 --- a/api/waf_rule.go +++ b/api/waf_rule.go @@ -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] diff --git a/model/request/waf_attack_log_req.go b/model/request/waf_attack_log_req.go index 51c761e..d4363e0 100644 --- a/model/request/waf_attack_log_req.go +++ b/model/request/waf_attack_log_req.go @@ -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 } diff --git a/model/request/waf_host_req.go b/model/request/waf_host_req.go index 6d78a13..7decc93 100644 --- a/model/request/waf_host_req.go +++ b/model/request/waf_host_req.go @@ -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 { diff --git a/model/request/waf_rule_req.go b/model/request/waf_rule_req.go index 6aa6698..6ec13f8 100644 --- a/model/request/waf_rule_req.go +++ b/model/request/waf_rule_req.go @@ -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 是关闭 } diff --git a/wafupgradenotice/upgrade_notes.yaml b/wafupgradenotice/upgrade_notes.yaml index 0680bb3..a2055f6 100644 --- a/wafupgradenotice/upgrade_notes.yaml +++ b/wafupgradenotice/upgrade_notes.yaml @@ -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.