From b4d843290df99135bb8523075541d42fd86b3690 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Sun, 23 Aug 2026 01:31:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/gin.go | 2 +- internal/server/handler/http.go | 2 +- internal/server/httpx/cookie.go | 27 ---------- internal/server/httpx/response.go | 29 ----------- internal/server/middleware/http.go | 2 +- internal/server/middleware/recovery.go | 57 +++++++++++++++++---- internal/server/middleware/recovery_test.go | 56 ++++++++++++++++++++ pkg/httpx/response.go | 26 ++++++++++ 8 files changed, 133 insertions(+), 68 deletions(-) delete mode 100644 internal/server/httpx/cookie.go delete mode 100644 internal/server/httpx/response.go create mode 100644 internal/server/middleware/recovery_test.go diff --git a/internal/server/gin.go b/internal/server/gin.go index c0fc041..f34795b 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -12,10 +12,10 @@ import ( "kra/internal/conf" websocket "kra/internal/integration/websocket" "kra/internal/server/handler" - "kra/internal/server/httpx" "kra/internal/server/middleware" "kra/internal/server/router" "kra/internal/service" + "kra/pkg/httpx" platformmodule "kra/pkg/module" "github.com/gin-gonic/gin" diff --git a/internal/server/handler/http.go b/internal/server/handler/http.go index 1dd4d98..2efa88c 100644 --- a/internal/server/handler/http.go +++ b/internal/server/handler/http.go @@ -2,8 +2,8 @@ package handler import ( "kra/internal/biz/system" - "kra/internal/server/httpx" "kra/internal/server/middleware" + httpx "kra/pkg/httpx" "github.com/gin-gonic/gin" ) diff --git a/internal/server/httpx/cookie.go b/internal/server/httpx/cookie.go deleted file mode 100644 index be82833..0000000 --- a/internal/server/httpx/cookie.go +++ /dev/null @@ -1,27 +0,0 @@ -package httpx - -import ( - "net/http" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -func requestUsesHTTPS(request *http.Request) bool { - if request.TLS != nil { - return true - } - forwarded := strings.SplitN(request.Header.Get("X-Forwarded-Proto"), ",", 2)[0] - return strings.EqualFold(strings.TrimSpace(forwarded), "https") -} - -// SetTokenCookie also marks cookies Secure behind TLS or a TLS-terminating -// reverse proxy. -func SetTokenCookie(c *gin.Context, value string, maxAge int) { - cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode} - if maxAge < 0 { - cookie.Expires = time.Unix(1, 0) - } - http.SetCookie(c.Writer, cookie) -} diff --git a/internal/server/httpx/response.go b/internal/server/httpx/response.go deleted file mode 100644 index 25598f2..0000000 --- a/internal/server/httpx/response.go +++ /dev/null @@ -1,29 +0,0 @@ -package httpx - -import ( - "github.com/gin-gonic/gin" - shared "kra/pkg/httpx" -) - -const ( - CodeSuccess = shared.CodeSuccess - CodeError = shared.CodeError - CodePasswordChangeRequired = shared.CodePasswordChangeRequired -) - -type Response = shared.Response -type PageResult = shared.PageResult - -func Write(c *gin.Context, code int, data any, message string) { - shared.Write(c, code, data, message) -} - -func OK(c *gin.Context) { shared.OK(c) } - -func OKWithData(c *gin.Context, data any) { shared.OKWithData(c, data) } - -func Fail(c *gin.Context, message string) { shared.Fail(c, message) } - -func NoAuth(c *gin.Context, message string) { - shared.NoAuth(c, message) -} diff --git a/internal/server/middleware/http.go b/internal/server/middleware/http.go index ceb2a5b..24d59a0 100644 --- a/internal/server/middleware/http.go +++ b/internal/server/middleware/http.go @@ -2,7 +2,7 @@ package middleware import ( "github.com/gin-gonic/gin" - "kra/internal/server/httpx" + httpx "kra/pkg/httpx" ) const ( diff --git a/internal/server/middleware/recovery.go b/internal/server/middleware/recovery.go index 2373962..7444205 100644 --- a/internal/server/middleware/recovery.go +++ b/internal/server/middleware/recovery.go @@ -1,10 +1,12 @@ package middleware import ( + "errors" "log/slog" "net" "net/http" "net/http/httputil" + "net/url" "os" "runtime/debug" "strings" @@ -19,16 +21,10 @@ func Recovery(logger *slog.Logger) gin.HandlerFunc { if panicValue == nil { return } - brokenPipe := false - if networkError, ok := panicValue.(*net.OpError); ok { - if syscallError, ok := networkError.Err.(*os.SyscallError); ok { - message := strings.ToLower(syscallError.Error()) - brokenPipe = strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer") - } - } - request, _ := httputil.DumpRequest(c.Request, false) + brokenPipe := isBrokenPipe(panicValue) + request := recoveryRequestDump(c.Request) if logger != nil { - logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "http", "error", panicValue, "request", string(request), "stack", string(debug.Stack())) + logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "http", "error", panicValue, "request", request, "stack", string(debug.Stack())) } if brokenPipe { if err, ok := panicValue.(error); ok { @@ -42,3 +38,46 @@ func Recovery(logger *slog.Logger) gin.HandlerFunc { c.Next() } } + +func isBrokenPipe(value any) bool { + var networkError *net.OpError + if !errors.As(asError(value), &networkError) { + return false + } + var syscallError *os.SyscallError + if !errors.As(networkError, &syscallError) { + return false + } + message := strings.ToLower(syscallError.Error()) + return strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer") +} + +func asError(value any) error { + err, _ := value.(error) + return err +} + +func recoveryRequestDump(request *http.Request) string { + if request == nil { + return "" + } + clone := request.Clone(request.Context()) + clone.Header = request.Header.Clone() + for key := range clone.Header { + switch strings.ToLower(key) { + case "authorization", "cookie", "set-cookie", "x-token", "proxy-authorization": + clone.Header[key] = []string{"***"} + } + } + if request.URL != nil { + clone.URL = new(url.URL) + *clone.URL = *request.URL + clone.URL.RawQuery = redactQuery(request.URL.RawQuery) + clone.RequestURI = clone.URL.RequestURI() + } + dumped, err := httputil.DumpRequest(clone, false) + if err != nil { + return clone.Method + " " + clone.RequestURI + } + return string(dumped) +} diff --git a/internal/server/middleware/recovery_test.go b/internal/server/middleware/recovery_test.go new file mode 100644 index 0000000..7ad56a8 --- /dev/null +++ b/internal/server/middleware/recovery_test.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "bytes" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "os" + "strings" + "syscall" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestRecoveryRedactsCredentialsFromRequestDump(t *testing.T) { + gin.SetMode(gin.TestMode) + var output bytes.Buffer + logger := slog.New(slog.NewTextHandler(&output, nil)) + engine := gin.New() + engine.Use(Recovery(logger)) + engine.GET("/panic", func(*gin.Context) { panic("boom") }) + + request := httptest.NewRequest(http.MethodGet, "/panic?token=query-secret&room=orders", nil) + request.Header.Set("Authorization", "Bearer header-secret") + request.Header.Set("Cookie", "x-token=cookie-secret") + request.Header.Set("X-Trace-Id", "trace-id") + response := httptest.NewRecorder() + engine.ServeHTTP(response, request) + + if response.Code != http.StatusInternalServerError { + t.Fatalf("status=%d, want %d", response.Code, http.StatusInternalServerError) + } + logText := output.String() + for _, secret := range []string{"query-secret", "header-secret", "cookie-secret"} { + if strings.Contains(logText, secret) { + t.Fatalf("recovery log leaked %q: %s", secret, logText) + } + } + if !strings.Contains(logText, "token=%2A%2A%2A") || !strings.Contains(logText, "Authorization: ***") || !strings.Contains(logText, "Cookie: ***") { + t.Fatalf("recovery log did not contain expected redaction: %s", logText) + } +} + +func TestRecoveryDetectsWrappedBrokenPipe(t *testing.T) { + pipe := &net.OpError{Op: "write", Net: "tcp", Err: os.NewSyscallError("write", syscall.EPIPE)} + if !isBrokenPipe(wrappedBrokenPipeError{cause: pipe}) { + t.Fatal("wrapped broken-pipe error was not detected") + } +} + +type wrappedBrokenPipeError struct{ cause error } + +func (wrappedBrokenPipeError) Error() string { return "wrapped broken pipe" } +func (e wrappedBrokenPipeError) Unwrap() error { return e.cause } diff --git a/pkg/httpx/response.go b/pkg/httpx/response.go index a4d4351..c2522d9 100644 --- a/pkg/httpx/response.go +++ b/pkg/httpx/response.go @@ -5,6 +5,8 @@ package httpx import ( "net/http" + "strings" + "time" "github.com/gin-gonic/gin" ) @@ -41,3 +43,27 @@ func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message func NoAuth(c *gin.Context, message string) { c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message}) } + +func requestUsesHTTPS(request *http.Request) bool { + if request != nil && request.TLS != nil { + return true + } + if request == nil { + return false + } + forwarded := strings.SplitN(request.Header.Get("X-Forwarded-Proto"), ",", 2)[0] + return strings.EqualFold(strings.TrimSpace(forwarded), "https") +} + +// SetTokenCookie is the shared transport-level cookie policy. It has no KRA +// business dependency, so handlers and middleware can use pkg/httpx directly. +func SetTokenCookie(c *gin.Context, value string, maxAge int) { + if c == nil { + return + } + cookie := &http.Cookie{Name: "x-token", Value: value, Path: "/", MaxAge: maxAge, Secure: requestUsesHTTPS(c.Request), HttpOnly: true, SameSite: http.SameSiteStrictMode} + if maxAge < 0 { + cookie.Expires = time.Unix(1, 0) + } + http.SetCookie(c.Writer, cookie) +}