统一严格解析管理接口 JSON

This commit is contained in:
xuans 2026-08-30 20:10:06 +08:00
parent e178f9fcf0
commit be84ecfb91
7 changed files with 104 additions and 22 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}