diff --git a/app/admin/apis/devops/app.go b/app/admin/apis/devops/app.go index a95dffda45665941c1f79779eb5d4e45964f26b0..427f561d0333a15286af9e7bb4807e6c2b7a1bba 100644 --- a/app/admin/apis/devops/app.go +++ b/app/admin/apis/devops/app.go @@ -140,6 +140,7 @@ func (a *AppApi) UploadPkg(c *gin.Context) { if err != nil { E.PanicErr(err) } + file.Filename = CleanFilename(file.Filename) if len([]rune(file.Filename)) > req.AppPkgFileNameMaxLength { R.Fail(c, "文件名太长", http.StatusBadRequest) return diff --git a/app/admin/apis/devops/explorer.go b/app/admin/apis/devops/explorer.go index a2ab1428b5317e9dcd70e25b9692e513b2c47563..cc6cfe298340f7944ec65b43c7c1e458f1ef9cc9 100644 --- a/app/admin/apis/devops/explorer.go +++ b/app/admin/apis/devops/explorer.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "path" + "regexp" "sort" "strings" ) @@ -74,6 +75,27 @@ func (a *ExplorerApi) DeleteEntry(c *gin.Context) { R.Success(c, nil) } +func FilenameCheck(filename string) error { + rgx := regexp.MustCompile("[^\\w.\\-@~]") + invalidStr := rgx.FindString(filename) + if invalidStr == "" { + if strings.HasPrefix(filename, "-") { + return E.Message("文件名不能以\"-\"开头") + } + return nil + } + return E.Message("不能包含特殊字符: " + invalidStr) +} + +func CleanFilename(filename string) string { + rgx := regexp.MustCompile("[^\\w.\\-@~]") + result := rgx.ReplaceAllString(filename, "_") + if strings.HasPrefix(filename, "-") { + result = strings.Replace(result, "-", "_", 1) + } + return result +} + // Upload godoc // // @Summary 上传文件 @@ -94,6 +116,7 @@ func (a *ExplorerApi) Upload(c *gin.Context) { if err != nil { E.PanicErr(err) } + file.Filename = CleanFilename(file.Filename) filePath := path.Clean(dir) + "/" + file.Filename if err = c.SaveUploadedFile(file, filePath); err != nil { if errors.Is(err, os.ErrPermission) { @@ -179,6 +202,9 @@ func (a *ExplorerApi) Rename(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { E.PanicErr(err) } + if err := FilenameCheck(body.NewName); err != nil { + E.PanicErr(err) + } oldPath := path.Clean(body.Dir + "/" + body.OldName) newPath := path.Clean(body.Dir + "/" + body.NewName) if oldPath == newPath { diff --git a/app/admin/apis/devops/explorer_sftp.go b/app/admin/apis/devops/explorer_sftp.go index b0a3702e9bb2e4a1759028366764721341e8056f..5448838ccd0c47255cef5f16e7280f23f6fbdea5 100644 --- a/app/admin/apis/devops/explorer_sftp.go +++ b/app/admin/apis/devops/explorer_sftp.go @@ -113,6 +113,7 @@ func (a *ExplorerSftpApi) Upload(c *gin.Context) { if err != nil { E.PanicErr(err) } + file.Filename = CleanFilename(file.Filename) sHostId := c.PostForm("hostId") hostId, err := strconv.Atoi(sHostId) if err != nil { @@ -212,6 +213,9 @@ func (a *ExplorerSftpApi) Rename(c *gin.Context) { if err := c.ShouldBindJSON(&body); err != nil { E.PanicErr(err) } + if err := FilenameCheck(body.NewName); err != nil { + E.PanicErr(err) + } host := a.getHost(body.HostId) if err := a.explorerSftpService.Rename(&body, host); err != nil { E.PanicErr(err) diff --git a/app/common/middleware/api_debug_logger.go b/app/common/middleware/api_debug_logger.go index d677ed7b58dd60be9ec0645407f474d690723af1..108f17557febdee6d42780a52a78df44c1455237 100644 --- a/app/common/middleware/api_debug_logger.go +++ b/app/common/middleware/api_debug_logger.go @@ -1,12 +1,11 @@ package middleware import ( - "bytes" "fmt" "github.com/MarchGe/go-admin-server/app/common/constant" + "github.com/MarchGe/go-admin-server/app/common/middleware/recorder" "github.com/gin-gonic/gin" "github.com/gobwas/glob" - "io" "log/slog" "path" "time" @@ -19,30 +18,34 @@ func initDebugPatterns(contextPath string) { contextPath + constant.Swagger + "/**", contextPath + "/terminal/ws", contextPath + "/terminal/ws/ssh/*", - contextPath + "/devops/app/upload", - contextPath + "/devops/explorer/upload", } } func ApiDebugLogger() gin.HandlerFunc { return func(c *gin.Context) { if ignoredDebug(c.Request.URL.Path) { + slog.Debug("==> Request info: ", slog.String("url", c.Request.URL.String())) c.Next() return } start := time.Now() - requestBodyBytes, _ := io.ReadAll(c.Request.Body) - c.Request.Body = io.NopCloser(bytes.NewReader(requestBodyBytes)) + requestBodyBytes, bodyIgnored := recorder.GetBodyContent(c) c.Next() end := time.Now() delay := end.Sub(start) - slog.Debug("RestApiInOutParameters", + var bodyLogAttr slog.Attr + if bodyIgnored { + bodyLogAttr = slog.Bool("bodyIgnored", true) + } else { + bodyLogAttr = slog.String("requestBody", string(requestBodyBytes)) + } + slog.Debug("==> Request info: ", slog.String("requestId", c.GetString(constant.RequestId)), slog.String("clientIp", c.ClientIP()), slog.String("method", c.Request.Method), slog.String("path", c.Request.URL.Path), slog.Any("query", c.Request.URL.Query()), - slog.String("requestBody", string(requestBodyBytes)), + bodyLogAttr, slog.Duration("duration", delay), ) } diff --git a/app/common/middleware/recorder/log_recorder.go b/app/common/middleware/recorder/log_recorder.go index d885aad60540e6ed06966bb999649a8e1e54c369..a36635abe06c4524764f0471ce337f4d8f5d7e47 100644 --- a/app/common/middleware/recorder/log_recorder.go +++ b/app/common/middleware/recorder/log_recorder.go @@ -13,10 +13,13 @@ import ( "github.com/gin-gonic/gin" "io" "log/slog" + "strconv" "strings" "time" ) +var logContentExceedLimit = 1024 + func RecordLoginLog(c *gin.Context, userId int64) { if !config.GetConfig().Log.LoginLog { return @@ -86,12 +89,23 @@ func RecordOpLog(opTarget string, v ...any) gin.HandlerFunc { log.Body = "[private]" } else { log.Query = c.Request.URL.Query().Encode() - if len(log.Query) > 255 { + if len(log.Query) > logContentExceedLimit { log.Query = "[too long ignored]" } - log.Body = getBodyParams(c) - if len(log.Body) > 500 { + contentLengthStr := c.GetHeader("Content-Length") + var contentLength int + if contentLengthStr != "" { + contentLength, _ = strconv.Atoi(contentLengthStr) + } + if contentLength > logContentExceedLimit { log.Body = "[too long ignored]" + } else { + body, ignored := GetBodyContent(c) + if ignored { + log.Body = "[ignored]" + } else { + log.Body = string(body) + } } } log.CreateTime = time.Now() @@ -124,12 +138,23 @@ func RecordExceptionLog(c *gin.Context, errString string) { } log.Path = c.Request.URL.Path log.Query = c.Request.URL.Query().Encode() - if len(log.Query) > 255 { + if len(log.Query) > logContentExceedLimit { log.Query = "[too long ignored]" } - log.Body = getBodyParams(c) - if len(log.Body) > 500 { + contentLengthStr := c.GetHeader("Content-Length") + var contentLength int + if contentLengthStr != "" { + contentLength, _ = strconv.Atoi(contentLengthStr) + } + if contentLength > logContentExceedLimit { log.Body = "[too long ignored]" + } else { + body, ignored := GetBodyContent(c) + if ignored { + log.Body = "[ignored]" + } else { + log.Body = string(body) + } } log.Error = errString log.CreateTime = time.Now() @@ -139,10 +164,19 @@ func RecordExceptionLog(c *gin.Context, errString string) { } } -func getBodyParams(c *gin.Context) string { - requestBodyBytes, _ := io.ReadAll(c.Request.Body) - c.Request.Body = io.NopCloser(bytes.NewReader(requestBodyBytes)) - return string(requestBodyBytes) +func GetBodyContent(c *gin.Context) (body []byte, bodyIgnored bool) { + contentType := c.GetHeader("Content-Type") + var requestBodyBytes []byte + var ignoreBody = true + if strings.Contains(contentType, "application/json") || strings.Contains(contentType, "application/x-www-form-urlencoded") || strings.Contains(contentType, "text/plain") { + requestBodyBytes, _ = io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewReader(requestBodyBytes)) + ignoreBody = false + } + if ignoreBody { + return nil, true + } + return requestBodyBytes, false } func parseMethod(method string) string { diff --git a/app/test/admin/apis/devops/explorer_test.go b/app/test/admin/apis/devops/explorer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d2a4cc32a804df723b96e462f25ebd8b360ef26b --- /dev/null +++ b/app/test/admin/apis/devops/explorer_test.go @@ -0,0 +1,46 @@ +package devops + +import ( + devops2 "github.com/MarchGe/go-admin-server/app/admin/apis/devops" + "testing" +) + +func TestFilenameCheck(t *testing.T) { + tests := []struct { + FileName string + Description string + ExpectedPass bool + }{ + {FileName: "-xxx.doc", Description: "测试文件名不能以“-”开头", ExpectedPass: false}, + {FileName: "xxx-.doc", Description: "测试文件名非开头可以包含“-”", ExpectedPass: true}, + {FileName: "xx!#x.doc", Description: "测试文件名不能包含特殊字符", ExpectedPass: false}, + } + for _, test := range tests { + err := devops2.FilenameCheck(test.FileName) + if (test.ExpectedPass && err == nil) || (!test.ExpectedPass && err != nil) { + t.Logf("\n-----\n用例描述:%s\n文件名: %s 预期:%t 结果:%s 原因:%v", test.Description, test.FileName, test.ExpectedPass, "一致", err) + } else { + t.Errorf("\n-----\n用例描述:%s\n文件名: %s 预期:%t 结果:%s 原因:%v", test.Description, test.FileName, test.ExpectedPass, "不一致", err) + } + } +} + +func TestCleanFilename(t *testing.T) { + tests := []struct { + FileName string + Description string + ExpectedOutput string + }{ + {FileName: "-xxx.doc", Description: "测试文件名不能以“-”开头", ExpectedOutput: "_xxx.doc"}, + {FileName: "xxx-.doc", Description: "测试文件名非开头可以包含“-”", ExpectedOutput: "xxx-.doc"}, + {FileName: "%xx!#x.d@%oc&", Description: "测试文件名不能包含特殊字符", ExpectedOutput: "_xx__x.d@_oc_"}, + } + for _, test := range tests { + fileName := devops2.CleanFilename(test.FileName) + if test.ExpectedOutput == fileName { + t.Logf("\n-----\n用例描述:%s\n文件名: %s 预期:%s 输出:%s 结果:%s", test.Description, test.FileName, test.ExpectedOutput, fileName, "一致") + } else { + t.Errorf("\n-----\n用例描述:%s\n文件名: %s 预期:%s 输出:%s 结果:%s", test.Description, test.FileName, test.ExpectedOutput, fileName, "不一致") + } + } +} diff --git a/cmd/server/server.go b/cmd/server/server.go index 636d4719ecbb95030e2df6589669a1fdb31e571f..3e1d1cd694a2f45d94705a344023944100b425f0 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -303,7 +303,7 @@ func getEngine() *gin.Engine { engine := gin.New() engine. - Use(middleware.ApiDebugLogger()). + //Use(middleware.ApiDebugLogger()). Use(middleware.GlobalErrHandler()). Use(middleware.SetRequestId()). Use(middleware.SetSession(&cfg.Cookie)).