diff --git a/backend/internal/httpapi/auth.go b/backend/internal/httpapi/auth.go index 9a7e0a2..c58a086 100644 --- a/backend/internal/httpapi/auth.go +++ b/backend/internal/httpapi/auth.go @@ -49,9 +49,8 @@ type authLoginRequest struct { } func (s *Server) authLogin(c *gin.Context) { - c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, authRequestBodyLimit) var request authLoginRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, authRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "登录请求参数无效", err) return } diff --git a/backend/internal/httpapi/json.go b/backend/internal/httpapi/json.go new file mode 100644 index 0000000..0505a36 --- /dev/null +++ b/backend/internal/httpapi/json.go @@ -0,0 +1,32 @@ +package httpapi + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" +) + +// bindJSON ???? JSON ?????????????????? JSON? +func bindJSON(c *gin.Context, target any, limit int64) error { + if c == nil || c.Request == nil || c.Request.Body == nil { + return errors.New("???????") + } + if limit > 0 { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit) + } + decoder := json.NewDecoder(c.Request.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return err + } + if err := decoder.Decode(&struct{}{}); err != io.EOF { + if err == nil { + return errors.New("????????? JSON ??") + } + return err + } + return nil +} diff --git a/backend/internal/httpapi/json_test.go b/backend/internal/httpapi/json_test.go new file mode 100644 index 0000000..04f0663 --- /dev/null +++ b/backend/internal/httpapi/json_test.go @@ -0,0 +1,61 @@ +package httpapi + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestBindJSONRejectsUnknownFieldsAndTrailingValues(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + body string + want bool + }{ + {name: "valid", body: "{\"value\":\"ok\"}\n", want: false}, + {name: "unknown field", body: "{\"value\":\"ok\",\"extra\":true}", want: true}, + {name: "trailing json", body: "{\"value\":\"ok\"}{\"value\":\"again\"}", want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gin.SetMode(gin.TestMode) + recorder := httptest.NewRecorder() + context, _ := gin.CreateTestContext(recorder) + context.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body)) + var payload struct { + Value string `json:"value"` + } + err := bindJSON(context, &payload, 1024) + if (err != nil) != tt.want { + t.Fatalf("bindJSON error = %v, want error %v", err, tt.want) + } + if !tt.want && payload.Value != "ok" { + t.Fatalf("valid payload was not decoded: %+v", payload) + } + if recorder.Code != http.StatusOK { + t.Fatalf("unexpected response status: %d", recorder.Code) + } + }) + } +} + +func TestBindJSONHonorsBodyLimit(t *testing.T) { + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(httptest.NewRecorder()) + body := `{"value":"` + string(bytes.Repeat([]byte("x"), 128)) + `"}` + context.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(body)) + var payload struct { + Value string `json:"value"` + } + err := bindJSON(context, &payload, 64) + var tooLarge *http.MaxBytesError + if !errors.As(err, &tooLarge) { + t.Fatalf("bindJSON error = %v, want MaxBytesError", err) + } +} diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index d30d71b..17b9d4b 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -332,9 +332,8 @@ func (s *Server) listDomains(c *gin.Context) { } func (s *Server) createDomain(c *gin.Context) { - limitJSONBody(c) var request domainRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } @@ -355,9 +354,8 @@ func (s *Server) createDomain(c *gin.Context) { } func (s *Server) updateDomain(c *gin.Context) { - limitJSONBody(c) var request domainRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } @@ -466,13 +464,12 @@ func (s *Server) getServer(c *gin.Context) { } func (s *Server) createServer(c *gin.Context) { - limitJSONBody(c) if s.servers == nil { s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置")) return } var request serverRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } @@ -490,13 +487,12 @@ func (s *Server) createServer(c *gin.Context) { } func (s *Server) updateServer(c *gin.Context) { - limitJSONBody(c) if s.servers == nil { s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置")) return } var request serverRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } @@ -570,7 +566,6 @@ func (s *Server) listDomainServers(c *gin.Context) { } func (s *Server) replaceDomainServers(c *gin.Context) { - limitJSONBody(c) if s.servers == nil { s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置")) return @@ -584,7 +579,7 @@ func (s *Server) replaceDomainServers(c *gin.Context) { return } var request domainServersRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "请求参数无效", err) return } diff --git a/backend/internal/httpapi/settings_api.go b/backend/internal/httpapi/settings_api.go index 7586ad0..8440a06 100644 --- a/backend/internal/httpapi/settings_api.go +++ b/backend/internal/httpapi/settings_api.go @@ -150,13 +150,12 @@ func (s *Server) getSettings(c *gin.Context) { } func (s *Server) updateSettings(c *gin.Context) { - limitJSONBody(c) if strings.TrimSpace(s.configPath) == "" { s.writeError(c, http.StatusInternalServerError, "配置文件路径未配置", errors.New("配置文件路径为空")) return } var request settingsRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "设置请求格式不正确", err) return } diff --git a/backend/internal/httpapi/setup.go b/backend/internal/httpapi/setup.go index 3b31e1f..08c013c 100644 --- a/backend/internal/httpapi/setup.go +++ b/backend/internal/httpapi/setup.go @@ -136,9 +136,8 @@ func (s *Server) testSetupDNS(c *gin.Context) { s.writeError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil) return } - c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, setupRequestLimit) var request dnsTestRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, setupRequestLimit); err != nil { s.writeError(c, http.StatusBadRequest, "DNS 测试请求参数无效", err) return } @@ -184,9 +183,8 @@ func (s *Server) applySetup(c *gin.Context) { s.writeError(c, http.StatusConflict, "初始化已经完成,不能重复提交", nil) return } - c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, setupRequestLimit) var request setupRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, setupRequestLimit); err != nil { s.writeError(c, http.StatusBadRequest, "初始化请求参数无效", err) return } diff --git a/backend/internal/httpapi/users_api.go b/backend/internal/httpapi/users_api.go index 14073e3..163e053 100644 --- a/backend/internal/httpapi/users_api.go +++ b/backend/internal/httpapi/users_api.go @@ -79,13 +79,12 @@ func (s *Server) getUser(c *gin.Context) { } func (s *Server) createUser(c *gin.Context) { - limitJSONBody(c) if s.users == nil { s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置")) return } var request userRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "用户参数无效", err) return } @@ -103,7 +102,6 @@ func (s *Server) createUser(c *gin.Context) { } func (s *Server) updateUser(c *gin.Context) { - limitJSONBody(c) if s.users == nil { s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置")) return @@ -119,7 +117,7 @@ func (s *Server) updateUser(c *gin.Context) { return } var request userRequest - if err := c.ShouldBindJSON(&request); err != nil { + if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil { s.writeError(c, http.StatusBadRequest, "用户参数无效", err) return }